├── .github └── workflows │ ├── tests-poetry.yml │ └── traffic.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cookiecutter.json ├── docs ├── terminalizer_template_setup.gif └── terminalizer_template_setup.yml ├── hooks ├── post_gen_project.py └── pre_gen_project.py ├── poetry.lock ├── pyproject.toml ├── renovate.json ├── tests ├── __init__.py ├── test_options.py ├── test_validation.py ├── util.py └── version_check.py └── {{cookiecutter.project_slug}} ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── .idea__editor ├── modules.xml └── {{cookiecutter.project_slug}}.iml ├── .pre-commit-config.yaml ├── .vscode__editor └── settings.json ├── Dockerfile__conda ├── Dockerfile__pip ├── Dockerfile__poetry ├── Dockerfile__uv ├── README.md ├── config ├── config.yml ├── dev.conf └── prod.conf ├── data └── .gitkeep ├── docker-compose.yml ├── environment-dev.yml ├── environment.yml ├── notebooks └── example.ipynb ├── poetry.toml ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── src └── {{cookiecutter.module_name}} │ ├── __init__.py │ ├── __main__.py │ ├── main.py │ ├── main__cli.py │ ├── res │ └── default.conf │ ├── util__hocon.py │ ├── util__yaml.py │ └── version.py └── tests ├── __init__.py ├── pytest.ini ├── test_example.py └── test_util.py /.github/workflows/tests-poetry.yml: -------------------------------------------------------------------------------- 1 | name: pytest-poetry 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '*.md' 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - '*.md' 14 | 15 | jobs: 16 | build: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | python-version: [3.9, 3.13] 22 | os: [ubuntu-latest, windows-latest] 23 | name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }}) 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install Poetry 33 | run: | 34 | curl -sSL https://install.python-poetry.org | python3 - 35 | shell: bash 36 | 37 | - name: Add Poetry to PATH (non-Windows) 38 | if: runner.os != 'Windows' 39 | shell: bash 40 | run: | 41 | echo "$HOME/.local/bin" >> $GITHUB_PATH 42 | 43 | - name: Add Poetry to PATH (Windows) 44 | if: runner.os == 'Windows' 45 | shell: pwsh 46 | run: | 47 | echo 'C:\Users\runneradmin\AppData\Roaming\Python\Scripts' >> $env:GITHUB_PATH 48 | 49 | - name: Install dependencies 50 | run: | 51 | poetry install --no-interaction --no-root 52 | 53 | - name: Test with pytest 54 | run: | 55 | poetry run pytest tests 56 | -------------------------------------------------------------------------------- /.github/workflows/traffic.yml: -------------------------------------------------------------------------------- 1 | name: github-traffic 2 | 3 | on: 4 | schedule: 5 | # runs once a week on sunday 6 | - cron: "55 23 * * 0" 7 | 8 | jobs: 9 | # This workflow contains a single job called "traffic" 10 | traffic: 11 | # The type of runner that the job will run on 12 | runs-on: ubuntu-latest 13 | 14 | # Steps represent a sequence of tasks that will be executed as part of the job 15 | steps: 16 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 17 | - uses: actions/checkout@v4 18 | with: 19 | ref: "traffic" 20 | 21 | # Calculates traffic and clones and stores in CSV file 22 | - name: GitHub traffic 23 | uses: sangonzal/repository-traffic-action@v0.1.5 24 | env: 25 | TRAFFIC_ACTION_TOKEN: ${{ secrets.TRAFFIC_ACTION_TOKEN }} 26 | 27 | # Commits files to repository 28 | - name: Commit changes 29 | uses: EndBug/add-and-commit@v9.1.4 30 | with: 31 | token: ${{ secrets.TRAFFIC_ACTION_TOKEN }} 32 | author_name: GH-Action-Traffic 33 | message: "GitHub traffic" 34 | add: "./traffic/*" 35 | branch: "traffic" 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # Unit test / coverage reports 29 | htmlcov/ 30 | junit/ 31 | .tox/ 32 | .coverage 33 | .coverage.* 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | *.cover 38 | .hypothesis/ 39 | .pytest_cache/ 40 | 41 | # Django stuff: 42 | *.log 43 | local_settings.py 44 | 45 | # Flask stuff: 46 | instance/ 47 | .webassets-cache 48 | 49 | # Sphinx documentation 50 | docs/_build/ 51 | 52 | # PyBuilder 53 | target/ 54 | 55 | # Jupyter Notebook 56 | .ipynb_checkpoints 57 | 58 | # pyenv 59 | .python-version 60 | 61 | # virtualenv 62 | .env 63 | .venv 64 | venv*/ 65 | 66 | # OS specific 67 | .DS_Store 68 | 69 | # IDE settings 70 | .vscode/ 71 | .idea/ 72 | *.iws 73 | 74 | # project-specific stuff 75 | serve/ 76 | cookiecutter-server.yml 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: 'v5.0.0' 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-merge-conflict 7 | - id: end-of-file-fixer 8 | - id: mixed-line-ending 9 | - id: trailing-whitespace 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - Placeholder for future updates and new features. 12 | 13 | ## [1.2.1] - 2025-06-05 14 | ### Changed 15 | 16 | - Updated pytest from 8.3.4 to 8.4 17 | - Updated typer from 0.15.1 to 0.16.0 18 | - Updated pre-commit hook assotile/pyugrade from v3.19.1 to v3.20.0 19 | - Updated setuptools from 76.0.0 to 80.9.0 20 | - Updated Python Docker tags from 3.11-slim-bookworm to 3.13-slim-bookworm and 3.11-bullseye to 3.13-bullseye 21 | - Updated pre-commit ruff from v0.11.0 to v0.11.12 22 | 23 | ## [1.2.0] - 2025-04-09 24 | 25 | ### Added 26 | - Introduced support for a new package manager `uv` in `cookiecutter.json`, `hooks/post_gen_project.py`, and `README.md`. 27 | - Added `Dockerfile__uv` for `uv` package manager support. 28 | 29 | ### Changed 30 | - Updated project version from `1.1.1` to `1.2.0` in `pyproject.toml`. 31 | - Updated `ruff` pre-commit hook from `v0.9.10` to `v0.11.0` in `.pre-commit-config.yaml`. 32 | - Updated `ruff` dependency in `environment-dev.yml` from `>=0.9.7` to `>=0.11.0`. 33 | - Updated `pytest` from `^8.3.4` to `^8.3.5` in `pyproject.toml`. 34 | - Updated `ruff` from `^0.9.7` to `^0.11.0` in `pyproject.toml`. 35 | - Updated `black` from `~22.10` to `~25.1.0` in `requirements-dev.txt`. 36 | - Updated `pytest` from `~8.3` to `~8.3.5` in `requirements-dev.txt`. 37 | - Updated `wheel` from `~0.37` to `~0.45.1` in `requirements-dev.txt`. 38 | 39 | ### Documentation 40 | - Added detailed instructions for using `uv` package manager in `README.md`, including installation, dependency management, running scripts, and testing. 41 | - Updated the README with new commands for `uv`, `poetry`, and `pip` package managers for running tests, building distribution packages, and setting up pre-commit hooks. 42 | 43 | ### Fixed 44 | - Corrected minor formatting issues in the `pyproject.toml` and `README.md`. 45 | 46 | ### Other 47 | - Updated `.dockerignore` to include additional directories and files. 48 | - Updated `docker-compose.yml` to support `uv` package manager. 49 | - Updated `requirements-dev.txt` for `black`, `pytest`, and `wheel` versions. 50 | 51 | ## [1.1.1] - 2025-03-13 52 | 53 | ### Changed 54 | - Updated dependency `setuptools` to v76. 55 | - Updated dependency `pytest` to v8.3.5. 56 | - Updated dependency `typer` to v0.15.2. 57 | - Updated pre-commit hook `astral-sh/ruff-pre-commit` to v0.9.10. 58 | 59 | 60 | ## [1.1.0] - 2025-02-26 61 | 62 | ### Added 63 | 64 | - Adjusted Python version to be at least 3.9 in `pyproject.toml` and `environment-dev.yml`. 65 | - Adjusted GitHub Actions to only use `poetry` for dependency management. 66 | - Refactored utility code by replacing `pkg_resources` with `importlib.resources`. 67 | - Added support for `pytest-cov` v6. 68 | - Added test coverage reporting to the GitHub Actions pipeline. 69 | - Updated Conda Docker tag to `v24` in `Dockerfile`. 70 | - Updated `actions/checkout` GitHub action to v4. 71 | - Updated pre-commit hooks, including: 72 | - `pycqa/isort` to v5.13.2. 73 | - `pre-commit/pre-commit-hooks` to v4.6.0. 74 | - `astral-sh/ruff-pre-commit` to v0.9.7. 75 | - `asottile/pyupgrade` to v3.19.1. 76 | - Updated `requirements.txt` in the cookiecutter project. 77 | - Made use of Python 3.9 and newer versions in `pyproject.toml`. 78 | - Upgraded dependencies in `pyproject.toml` to ensure the project uses the latest versions. 79 | - Updated and aligned `environment-dev.yml` and `environment.yml` with `pyproject.toml` for consistency. 80 | - Updated versions of GitLab CI python and poetry versions. 81 | - Fixed pre-commit hook for `black` installation via pip with the correct version constraint (`>=`). 82 | 83 | ### Fixed 84 | 85 | - Fixed test since `black` is installed by pip, and updated the test to check for the correct version format (`>=`). 86 | - Fixed coverage reporting integration in GitHub Actions. 87 | 88 | ## [1.0.1] - 2025-02-26 89 | 90 | ### Fixed 91 | - **Conda package caching in GitLab CI:** Updated caching paths in `{{ cookiecutter.project_slug }}/.gitlab-ci.yml` to include the new `.conda` file format and additional cache directories. The updated paths now cache: 92 | - `$PIP_CACHE_DIR` 93 | - `$CONDA_PKGS_DIRS/*.conda` 94 | - `$CONDA_PKGS_DIRS/*.tar.bz2` 95 | - `$CONDA_PKGS_DIRS/urls*` 96 | - `$CONDA_PKGS_DIRS/cache` 97 | 98 | This change ensures that all relevant Conda packages and related metadata are properly cached, addressing issues with the previous configuration ([Conda docs](https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html#force-conda-to-download-only-tar-bz2-packages-use-only-tar-bz2), [Damiankula’s guide](https://damiankula.com/using_conda_cache_in_gitlabci.html)). cc @ChrsBaur 99 | 100 | ## [1.0.0] - 2025-02-26 101 | 102 | ### Added 103 | - **Initial stable release** of the AT Python Template. 104 | - **Modular project structure** using `src/` directory for better code organization. 105 | - **Flexible package management** with options for `pip`, `poetry`, and `conda`. 106 | - **Automated project setup** using `cookiecutter` to streamline project generation. 107 | - **Pre-configured CLI support** via `typer`, enabling command-line applications. 108 | - **Pre-commit hooks** with `ruff` for formatting and linting. 109 | - **Docker integration** with `Dockerfile` and `docker-compose.yml` for containerized deployment. 110 | - **Automated testing setup** using `pytest` for unit testing. 111 | - **Jupyter Notebook compatibility** with a dedicated `notebooks/` folder. 112 | - **Configuration management** using `config/` directory and YAML/HOCON formats. 113 | - **Editor support** with settings for **VS Code** and **PyCharm**. 114 | - **Multiple CI/CD integrations** including **GitHub Actions** and **GitLab CI**. 115 | - **Automated dependency updates** using `renovate`. 116 | - **Updated documentation** covering installation, project structure, and setup steps. 117 | - **Human-readable prompts** improving user experience during project generation. 118 | - **Versioning enforcement** requiring **Python 3.8+** while dropping Python 3.7 support. 119 | 120 | ### Changed 121 | - **Refactored codebase** to ensure maintainability and modularity. 122 | - **Updated default dependencies** for better performance and security. 123 | - **Enhanced user prompts** for a more intuitive project setup. 124 | - **Switched to f-strings** replacing old `.format()` calls for improved readability. 125 | - **Simplified installation steps** by improving `README.md` structure. 126 | - **Improved GitHub Actions workflows** for automated testing and validation. 127 | 128 | ### Fixed 129 | - **Resolved setup issues** related to module placement under `src/`. 130 | - **Fixed broken paths and URLs** in documentation. 131 | - **Ensured pre-commit hooks work as expected** with proper configurations. 132 | - **Addressed whitespace inconsistencies** in generated files. 133 | 134 | --- 135 | 136 | [Unreleased]: https://github.com/at-gmbh/at-python-template/compare/v1.0.0...HEAD 137 | [1.0.0]: https://github.com/at-gmbh/at-python-template/releases/tag/v1.0.0 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to AT Python Template 2 | 3 | We welcome your contributions! Whether it's reporting a bug, discussing ideas, submitting fixes, or proposing new features, we appreciate your input. 4 | 5 | ## We Develop with GitHub 6 | We use GitHub to host code, track issues, and review pull requests. 7 | 8 | ## Follow the GitHub Flow 9 | All changes should be proposed through pull requests: 10 | 11 | 1. Fork the repository and create your branch from `master`. 12 | 2. If you've added new functionality, include tests. 13 | 3. Update the documentation if there are any changes to APIs or usage. 14 | 4. Ensure all tests pass. 15 | 5. Verify that your code adheres to linting and formatting standards. 16 | 6. Submit your pull request! 17 | 18 | ## Code License 19 | When you submit code changes, you agree that your contributions will be licensed under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). 20 | 21 | ## Reporting Issues 22 | Please use [GitHub Issues](https://github.com/at-gmbh/at-python-template/issues) to report bugs and suggest improvements. Make sure to include: 23 | 24 | - A brief summary of the issue 25 | - Steps to reproduce the problem 26 | - Expected behavior 27 | - Actual behavior 28 | - Additional notes or related references 29 | 30 | ## Coding Standards 31 | 32 | - Use 4 spaces per indentation level 33 | - Maximum line length is 120 characters 34 | - Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guidelines 35 | - Use [Ruff](https://github.com/charliermarsh/ruff) for formatting and linting (applied automatically with pre-commit hooks, or manually if needed) 36 | - Include type hints as per [PEP 484](https://peps.python.org/pep-0484/) 37 | - Use Google-style docstrings 38 | - Ensure code is clean, readable, and maintainable 39 | 40 | ## Imports 41 | 42 | - Order imports as follows: 43 | 1. Future imports 44 | 2. Standard library imports 45 | 3. Third-party imports 46 | 4. Local imports 47 | - Avoid wildcard imports 48 | - Use absolute imports unless relative imports simplify the code 49 | 50 | ## Comments 51 | 52 | - Write comments in English 53 | - Explain the intention behind code, not what the code does 54 | - Use full sentences with proper capitalization and punctuation 55 | 56 | ## Naming Conventions 57 | 58 | - Variables and functions: `lower_case_with_underscores` 59 | - Classes: `CapWords` 60 | - Constants: `ALL_CAPS_WITH_UNDERSCORES` 61 | - Modules: `alllowercase` 62 | 63 | ## Setting Up for Development 64 | 65 | 1. Install dependencies using `poetry install`. 66 | 2. Run tests with `pytest tests`. 67 | 3. Use `pre-commit install` to set up pre-commit hooks. This ensures that Ruff and other checks run before each commit. 68 | 69 | ## Testing 70 | We use [pytest](https://docs.pytest.org/) for running tests. Ensure that all tests pass before submitting a pull request. 71 | 72 | ## Additional Resources 73 | 74 | - [Cookiecutter Documentation](https://cookiecutter.readthedocs.io/) 75 | - [GitHub Flow Guide](https://guides.github.com/introduction/flow/) 76 | - [Python Packaging Guide](https://packaging.python.org/) 77 | - [Poetry Documentation](https://python-poetry.org/) 78 | - [Ruff Documentation](https://docs.astral.sh/ruff/) 79 | - [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) 80 | 81 | Thank you for your contributions! 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AT Python Template 2 | 3 | [![Build](https://img.shields.io/github/actions/workflow/status/at-gmbh/at-python-template/tests-poetry.yml?branch=master)](https://github.com/at-gmbh/at-python-template/actions?query=branch%3Amaster+) 4 | ![Python Version](https://img.shields.io/badge/python-3.9%20--%203.13-blue) 5 | [![Latest Version](https://img.shields.io/github/v/tag/at-gmbh/at-python-template?label=version&sort=semver)](https://github.com/at-gmbh/at-python-template/releases) 6 | [![Commit Activity](https://img.shields.io/github/commit-activity/m/at-gmbh/at-python-template)](https://github.com/at-gmbh/at-python-template/commits/master) 7 | [![Dependencies](https://img.shields.io/librariesio/github/at-gmbh/at-python-template)](https://libraries.io/github/at-gmbh/at-python-template) 8 | [![Open Issues](https://img.shields.io/github/issues/at-gmbh/at-python-template)](https://github.com/at-gmbh/at-python-template/issues) 9 | [![Contributors](https://img.shields.io/github/contributors/at-gmbh/at-python-template)](https://github.com/at-gmbh/at-python-template/graphs/contributors) 10 | [![License](https://img.shields.io/github/license/at-gmbh/at-python-template)](https://github.com/at-gmbh/at-python-template/blob/master/LICENSE) 11 | [![Last Commit](https://img.shields.io/github/last-commit/at-gmbh/at-python-template)](https://github.com/at-gmbh/at-python-template/commits/master) 12 | 13 | 14 | 15 | This is the official Python Project Template of Alexander Thamm GmbH (AT). It is built with [cookiecutter](https://cookiecutter.readthedocs.io/) and inspired by [cookiecutter-data-science](https://github.com/drivendata/cookiecutter-data-science). It is designed to bridge the gap between exploratory work and production-ready projects. The goal is to be able to quickly bootstrap a Python project that provides lots of useful stuff to build, test & deploy your code without being overly convoluted like many of the publicly available packages. 16 | 17 | This guide aims to help you set up your environment and get coding in no time. 18 | 19 | ## 🛠 Prerequisites 20 | 21 | Before you dive in, make sure you have Python 3.9 or higher installed on your machine. To check if Python is installed, open a terminal and run: 22 | 23 | ``` 24 | python --version 25 | ``` 26 | 27 | If Python is installed correctly, you'll see the version number. Otherwise, you'll need to [install Python](https://www.python.org/downloads/). 28 | 29 | ## 🚀 Getting Started 30 | 31 | ### Step 1: Install `cookiecutter` 32 | 33 | Make sure you have version 2.0 or higher of `cookiecutter`. 34 | 35 | #### Via Conda 🐍 36 | 37 | ``` 38 | conda install -c conda-forge "cookiecutter>=2.0" 39 | ``` 40 | 41 | #### Via Pip 📦 42 | 43 | ``` 44 | pip install -U "cookiecutter>=2.0" 45 | ``` 46 | 47 | ### Step 2: Generate Your Project 48 | 49 | Run the following command to generate your project structure. 50 | 51 | ``` 52 | cookiecutter https://github.com/at-gmbh/at-python-template 53 | ``` 54 | After running this command, you will be asked questions like the video below is showing. In the section [Choices explained](#-choices-explained) you can find more information on each item. 55 | 56 | ![image](docs/terminalizer_template_setup.gif "Setup a professional python project in seconds") 57 | 58 | ### Step 3: Navigate to Your Project 59 | 60 | Change your directory to the newly created project. 61 | 62 | ``` 63 | cd your_project_name 64 | ``` 65 | 66 | ### Step 4: Install Your Project Locally 67 | 68 | Via pip: 69 | 70 | ``` 71 | pip install -e . 72 | ``` 73 | 74 | For other package managers like `poetry` or `conda`, adapt accordingly. 75 | 76 | ## 📓 Jupyter Notebook Setup 77 | 78 | If you're planning to use Jupyter Notebooks, you'll need to install Jupyter Lab. 79 | #### Via Conda 🐍 80 | 81 | ``` 82 | conda install -c conda-forge jupyterlab 83 | ``` 84 | 85 | #### Via Pip 📦 86 | 87 | ``` 88 | pip install jupyterlab 89 | ``` 90 | 91 | ## 🐳 Docker Setup 92 | 93 | If you've chosen to use Docker, here's how to build and run your project: 94 | 95 | ### Build your Docker image 96 | 97 | ``` 98 | docker build -t your-image-name . 99 | ``` 100 | 101 | ### Run your Docker container 102 | 103 | ``` 104 | docker run your-image-name 105 | ``` 106 | 107 | ### Using docker-compose 108 | 109 | ``` 110 | docker-compose up 111 | ``` 112 | 113 | For more advanced Docker usage, please refer to the `Dockerfile` and `docker-compose.yml` generated in your project. 114 | 115 | ## 🔗 Additional Dependencies 116 | 117 | - Python 3.9 or higher is required. 118 | - Jupyter Lab: If you are planning to use Jupyter notebooks. 119 | - This template requires `cookiecutter>=2.0`. If you experience issues installing it into your default conda environment, we recommend to create a new clean environment with nothing but the `cookiecutter` package installed. 120 | 121 | The automatically created `README.md` will contain notes on how to set up your local development environment. 122 | You can find more detailed guidelines on how to set up your local development environment in [PyCharm](https://www.jetbrains.com/pycharm/) or [Visual Studio Code](https://code.visualstudio.com/). 123 | 124 | Feedback and contributions are very welcome! Learn more in the [Contributing](#-contributing) section below. 125 | 126 | ## 🤔 Choices explained 127 | 128 | * `full_name [Jane Doe]`: enter your name here. It will be used in the Readme and the setup script. 129 | * `company_name []`: enter your company's name here. The default is to leave this blank. 130 | * `email [contact@alexanderthamm.com]`: your email address, also for *Readme.md* and *setup.py*. 131 | * `project_name [My Project]`: the name of your project. This is the "pretty" version of your project name, it may contain whitespace and all of your favorite emojis. 132 | * `project_slug [my-project]`: this is the project slug. It is automatically derived from your project name, uses dashes instead of whitespace and must not contain any special characters. This will also be the name of your Python package. 133 | * `module_name [my_project]`: this is the name of your Python module. It is similar to the project slug, but uses underscores instead of dashes. This is because for Python module names, the same restrictions apply as for Python variable names and `-` cannot be used in a variable name. 134 | * `project_short_description [a short summary of the project]`: please write a short summary (ideally one sentence) of your project here. It will be used in the Readme and the setup script. 135 | * Select your `package_manager` 136 | - `conda` (default): use [conda](https://docs.conda.io/) as your package manager and `environment.yml` to track your dependencies. Conda allows you to easily manage virtual environments and Python versions and there are hardly any issues with installing packages on Windows. Therefore we recommend this option for most users. 137 | - `pip`: use [pip](https://pip.pypa.io/) as your package manager and `requirements.txt` to track your dependencies. Unlinke conda, pip is only a package manager: it does not manage your Python environment and there might be some issues with package installation on Windows if no pre-compiled binaries are available. However, pip is lightweight, comes with every Python distribution and is the default in the Python community. Recommended for power users or projects that will be used in production environments or when Docker is your target (most Python-based Docker images don't include conda). 138 | - `poetry`: use [poetry](https://python-poetry.org/) as your package manager and `pyproject.toml` to track your dependencies and store package metadata. Poetry, like Conda, makes it easy to manage virtual environments and python versions. Poetry ensures that your build is reproducible, simplifies project configuration (there's only one file to keep track of), and makes it _hard_ to use the wrong virtual environment. On the other hand, Poetry doesn't let you install Conda packages. Recommended as an alternative to pip. 139 | * `use_notebooks` (yes or no): if you want to use Jupyter Notebooks, we'll set you up so that all your notebooks will be stored in the `notebooks` folder and you can easily call functions that are defined in your Python package from the notebooks. This should ease the transition from exploration (using notebooks) to production (using code specified in the module) and allow early sharing of code. 140 | * `use_docker` (yes or no): if you plan to build a Docker image, select *yes* and we'll set you up with a Dockerfile and other docker-related stuff. 141 | * Select your `ci_pipeline` 142 | - `none` (default): Don't use any CI/CD pipeline. 143 | - `gitlab`: If you plan to use GitLab, this option will add a CI/CD Pipeline definition for [GitLab CI/CD](https://docs.gitlab.com/ee/ci/). The pipeline includes basic steps to build, test and deploy your code. The deployment steps do nothing but echoing a String, as deployment is very project-specific. 144 | * `create_cli` (yes or no): if you plan to build an application with a command line interface (CLI), select *yes* here. This will integrate a template for the CLI into your project - minimal boilerplate guaranteed! (We're leveraging the awesome [typer](https://typer.tiangolo.com/) library for this.) 145 | * `config_file`: select your preferred config format. It is best practice to store your configuration separate from your code, even for small projects, but because there are a gazillion ways to do this, each project seems to reinvents the wheel. We want to provide a few options to set you up with a working configuration: 146 | - `yaml`: use [YAML](https://yaml.org/) as your configuration file format. Easy to read and write, widely adopted, relies on the [PyYAML](https://pyyaml.org/) package. 147 | - `hocon`: use [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) as your configuration file format. It is a superset of JSON, very resilient (it's really hard to make a breaking syntax error) and comes with powerful functions, e.g. for inheritance or variable substitution. In this example you can find two environment configurations (`dev.conf`, `prod.conf`) that override parts of the default configuration. Relies on the [pyhocon](https://github.com/chimpler/pyhocon/) package. 148 | - `none`: don't need any configuration or want to do your own thing? choose this option. 149 | * `code_formatter`: a code formatter is a powerful tool that can help teams to stick to a common code style. However, a formatter cannot solve every code style problem for you and it may lead to issues for users that are not aware of how it works. Always talk to your team about [PEP 8](https://www.python.org/dev/peps/pep-0008/) and a common code style, then choose the right formatter for you (or none at all): 150 | - [`black`](https://github.com/psf/black): *the uncompromising Python code formatter*. 151 | - `none`: don't use a code formatter. 152 | * `editor_settings`: there are many editors out there with great support for Python projects. However, because Python gives you a lot of freedom in terms of structuring your project or the tools you use, configuring your editor for each project can be intricate and repetitive. If your favourite editor is in the list, we'll get you started with a working config for your project. Please note that this config will not be checked into the git repo (that would be bad practice; every developer should be free to use whatever editor they like) and therefore this config will only be available to you. 153 | - `vscode`: add config for [Visual Studio Code](https://code.visualstudio.com/) 154 | - `pycharm`: add config for [PyCharm](https://www.jetbrains.com/pycharm/) 155 | - `none`: no editor config / set it up by yourself 156 | 157 | ## 🌟 Features 158 | 159 | * `README.md`: arguably *the* most important file of your project. It's the first thing anyone will see who looks at your project. Write it so that someone who doesn't know anything about your project can build the code, run tests and start working on the code after reading this document. Check out [makeareadme.com](https://www.makeareadme.com/) to learn about best practices and have a look at [awesome-readme](https://github.com/matiassingers/awesome-readme) for examples of awesome readme files. 160 | * Code structure 161 | - `src/{my_module}`: this is your module - all your Python code should be stored here. Make sure to add the `src` folder to your `PYTHONPATH` or tell your IDE that this is where your sources are, if it didn't figure that out by itself. While you could have your module folder at the root of your git repo, there are [some good reasons](https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure) why using a `src` folder may be the better solution. 162 | - `tests`: tests are defined under this folder. We use [pytest](https://docs.pytest.org/) as our test framework by default. 163 | - `notebooks`: drop your Jupyter notebooks in this folder and remember to move frequently used code to the module folder as soon as possible, or you'll end up with a bunch of spaghetti code spread out over way too many notebooks. 164 | - `config`: store your config files in this folder. Don't hesitate to create variants of your config files for different environments (e.g. debugging, integration, production) 165 | * setup 166 | - `setup.py`: this is the standard Python [Setup Script](https://setuptools.readthedocs.io/en/latest/setuptools.html). It allows you to install your module, create distribution packages and much more. 167 | - conda: 168 | * `environment.yml`: definition of an environment that can run the module. Does not include tests or developer tools. 169 | * `environment-dev.yml`: definition of an environment for developers. Please install to make full use of many useful features (e.g. tests, pre-commit hooks, etc.) 170 | - pip: 171 | * `requirements.txt`: dependencies that are required to run the module. Does not include tests or developer tools. 172 | * `requirements-dev.txt`: dependencies for developers. Please install to make full use of many useful features (e.g. tests, pre-commit hooks, etc.) 173 | - poetry: 174 | * `pyproject.toml`: project metadata, dependencies, and developer dependencies. See [here](https://python-poetry.org/docs/pyproject/) for full documentation. 175 | * Docker 176 | - `Dockerfile` 177 | - `docker-compose.yml` 178 | - `.dockerignore` 179 | * those files starting with a dot 180 | - `.gitignore` 181 | - [`.editorconfig`](https://editorconfig.org/): ensures that all your text files use the same style, e.g. tabs vs. spaces, indent size, trailing whitespace, and so on. Supported by [many popular editors](https://editorconfig.org/#download), but not everywhere. 182 | - `.pre-commit-config.yaml`: notifies you of common issues when you make a commit, like committing a huge binary file. Relies on the [`pre-commit`](https://pre-commit.com/) package. 183 | - settings for specific editors 184 | * `.vscode`: Settings for VSCode. Sets *pytest* as default test framework and configures automatic code formatting if requested, among other things. 185 | * `.idea`: Settings for PyCharm. Marks `./src` as source folder, sets *pytest* as default test framework and *reStructuredText* as default docstring format. 186 | 187 | ## 🤝 Contributing 188 | 189 | Contributions to this project are very welcome. Please open a ticket on the [issues page](https://github.com/at-gmbh/at-python-template/issues) if you have found any bugs or if you have ideas for improvements. If you want to contribute code, we'd love to review your [pull request](https://github.com/at-gmbh/at-python-template/pulls)! 190 | 191 | This Readme file contains our technical documentation, a user guide can be found in [Confluence](https://confluence.alexanderthamm.com/display/ATTECH/AT+Python+Template). Please don't hesitate to contact one of the persons listed at the end of this document to get started. Confluence and Teams can only be accessed by AT employees; however, you're very welcome to discuss issues, submit PRs and contact the developers by mail (see below) if you're not an AT employee. 192 | 193 | Hints for developers: 194 | 195 | * install dependencies in a fresh virtualenv with `pip install -r requirements.txt` 196 | * run unit tests with `pytest tests`. There are unit tests for every available choice in [`tests/test_options.py`](./tests/test_options.py). If you add more choices, please update these tests. 197 | * be careful with code formatters: Many files in this project contain [jinja2 templates](https://jinja.palletsprojects.com) (you'll find statements like `{% if cookiecutter.config_file == 'yaml' %}...{% endif %}` all over the place). These templates mean that the source code becomes syntactically incorrect and some formatters might do unexpected things. 198 | * you can make your life easier when updating templated files by using [cookiecutter-server](https://github.com/at-gmbh/cookiecutter-server) to get live previews of your templates 199 | * before your first commit, set up pre-commit hooks by running `pre-commit install` 200 | 201 | ## Other Templates 202 | 203 | You may find lots of other templates under the [cookiecutter-template](https://github.com/topics/cookiecutter-template) tag on GitHub. Some of the most popular templates (by Github Stars) can be found on [awesomeopensource.com](https://awesomeopensource.com/projects/cookiecutter). Other popular templates for data science are [cookiecutter-data-science](https://github.com/drivendata/cookiecutter-data-science) and the [pyscaffold dsproject](https://github.com/pyscaffold/pyscaffoldext-dsproject). 204 | 205 | ## License 206 | 207 | Copyright 2025 Alexander Thamm GmbH 208 | 209 | Licensed under the Apache License, Version 2.0 (the "License"); 210 | you may not use this file except in compliance with the License. 211 | You may obtain a copy of the License at 212 | 213 | http://www.apache.org/licenses/LICENSE-2.0 214 | 215 | Unless required by applicable law or agreed to in writing, software 216 | distributed under the License is distributed on an "AS IS" BASIS, 217 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 218 | See the License for the specific language governing permissions and 219 | limitations under the License. 220 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "full_name": "Jane Doe", 3 | "company_name": "", 4 | "email": "contact@alexanderthamm.com", 5 | "project_name": "My Project", 6 | "project_slug": "{{ cookiecutter.project_name | slugify }}", 7 | "module_name": "{{ cookiecutter.project_slug | slugify | replace('-', '_') }}", 8 | "project_short_description": "A short summary of the project", 9 | "package_manager": ["conda", "pip", "poetry","uv"], 10 | "use_notebooks": ["no", "yes"], 11 | "use_docker": ["no", "yes"], 12 | "ci_pipeline": ["none", "gitlab"], 13 | "create_cli": ["no", "yes"], 14 | "config_file": ["none", "hocon", "yaml"], 15 | "code_formatter": ["none", "black"], 16 | "editor_settings": ["none", "pycharm", "vscode"], 17 | "__prompts__": { 18 | "full_name": "What's your [bold yellow]name[/]?", 19 | "company_name": "Enter your [bold yellow]company name[/]; leave empty if not applicable", 20 | "email": "What's your [bold yellow]email address[/]?", 21 | "project_name": "Please provide the [bold yellow]full name of your project[/], as it would appear in a headline", 22 | "project_slug": "Please provide a [bold yellow]slug[/], which is the project name as it would appear in a URL, or accept this suggestion", 23 | "module_name": "Please provide a [bold yellow]module name[/] for your project. Make it short, if possible, and use underscores instead of whitespace", 24 | "project_short_description": "Provide a [bold yellow]short description[/] for the project in one sentence", 25 | "package_manager": { 26 | "__prompt__": "Which [bold yellow]packaging tool[/] would you like to use?", 27 | "conda": "conda (environment.yml)", 28 | "pip": "pip (setup.py)", 29 | "poetry": "poetry (pyproject.toml)" 30 | }, 31 | "use_notebooks": { 32 | "__prompt__": "Do you want to include [bold yellow]Jupyter Notebooks[/] in your project?", 33 | "no": "No", 34 | "yes": "Yes" 35 | }, 36 | "use_docker": { 37 | "__prompt__": "Do you want to use [bold yellow]Docker[/]?", 38 | "no": "No", 39 | "yes": "Yes" 40 | }, 41 | "ci_pipeline": { 42 | "__prompt__": "What [bold yellow]CI pipeline[/] would you like to use?", 43 | "none": "None", 44 | "gitlab": "GitLab CI" 45 | }, 46 | "create_cli": { 47 | "__prompt__": "Do you want to create a [bold yellow]CLI[/] for your project?", 48 | "no": "No", 49 | "yes": "Yes" 50 | }, 51 | "config_file": { 52 | "__prompt__": "Which [bold yellow]config file format[/] do you prefer?", 53 | "none": "None", 54 | "hocon": "HOCON", 55 | "yaml": "YAML" 56 | }, 57 | "code_formatter": { 58 | "__prompt__": "What [bold yellow]code formatter[/] would you like to use?", 59 | "none": "None", 60 | "black": "Black" 61 | }, 62 | "editor_settings": { 63 | "__prompt__": "Which [bold yellow]editor settings[/] do you want to include?", 64 | "none": "None", 65 | "pycharm": "PyCharm", 66 | "vscode": "VS Code" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/terminalizer_template_setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/at-gmbh/at-python-template/f167921eca9cf2bbac387a5890a2f93ca5304826/docs/terminalizer_template_setup.gif -------------------------------------------------------------------------------- /docs/terminalizer_template_setup.yml: -------------------------------------------------------------------------------- 1 | # The configurations that is used for the recording, feel free to edit them 2 | # to reduce the size of the gif, you may use gifsicle - http://www.lcdf.org/gifsicle/ 3 | # gifsicle --optimize=3 --colors 16 -i input.gif -o compressed.gif 4 | 5 | config: 6 | 7 | # Specify a command to be executed 8 | # like `/bin/bash -l`, `ls`, or any other commands 9 | # the default is bash for Linux 10 | # or powershell.exe for Windows 11 | command: bash -l 12 | 13 | # Specify the current working directory path 14 | # the default is the current working directory path 15 | cwd: /home/batman 16 | 17 | # Export additional ENV variables 18 | env: 19 | recording: true 20 | 21 | # Explicitly set the number of columns 22 | # or use `auto` to take the current 23 | # number of columns of your shell 24 | cols: 120 25 | 26 | # Explicitly set the number of rows 27 | # or use `auto` to take the current 28 | # number of rows of your shell 29 | rows: 40 30 | 31 | # Amount of times to repeat GIF 32 | # If value is -1, play once 33 | # If value is 0, loop indefinitely 34 | # If value is a positive number, loop n times 35 | repeat: 0 36 | 37 | # Quality 38 | # 1 - 100 39 | quality: 100 40 | 41 | # Delay between frames in ms 42 | # If the value is `auto` use the actual recording delays 43 | frameDelay: auto 44 | 45 | # Maximum delay between frames in ms 46 | # Ignored if the `frameDelay` isn't set to `auto` 47 | # Set to `auto` to prevent limiting the max idle time 48 | maxIdleTime: 2000 49 | 50 | # The surrounding frame box 51 | # The `type` can be null, window, floating, or solid` 52 | # To hide the title use the value null 53 | # Don't forget to add a backgroundColor style with a null as type 54 | frameBox: 55 | type: solid 56 | title: null 57 | style: 58 | border: 0px black solid 59 | boxShadow: none 60 | margin: 0px 61 | 62 | # Add a watermark image to the rendered gif 63 | # You need to specify an absolute path for 64 | # the image on your machine or a URL, and you can also 65 | # add your own CSS styles 66 | watermark: 67 | imagePath: null 68 | style: 69 | position: absolute 70 | right: 15px 71 | bottom: 15px 72 | width: 100px 73 | opacity: 0.9 74 | 75 | # Cursor style can be one of 76 | # `block`, `underline`, or `bar` 77 | cursorStyle: block 78 | 79 | # Font family 80 | # You can use any font that is installed on your machine 81 | # in CSS-like syntax 82 | fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" 83 | 84 | # The size of the font 85 | fontSize: 15 86 | 87 | # The height of lines 88 | lineHeight: 1 89 | 90 | # The spacing between letters 91 | letterSpacing: 0 92 | 93 | # Theme 94 | theme: 95 | background: "transparent" 96 | foreground: "#afafaf" 97 | cursor: "#c7c7c7" 98 | black: "#232628" 99 | red: "#fc4384" 100 | green: "#b3e33b" 101 | yellow: "#ffa727" 102 | blue: "#75dff2" 103 | magenta: "#ae89fe" 104 | cyan: "#708387" 105 | white: "#d5d5d0" 106 | brightBlack: "#626566" 107 | brightRed: "#ff7fac" 108 | brightGreen: "#c8ed71" 109 | brightYellow: "#ebdf86" 110 | brightBlue: "#75dff2" 111 | brightMagenta: "#ae89fe" 112 | brightCyan: "#b1c6ca" 113 | brightWhite: "#f9f9f4" 114 | 115 | # Records, feel free to edit them 116 | records: 117 | - delay: 100 118 | content: "\e]0;batman@batbook:~\e\\" 119 | - delay: 100 120 | content: "\e]0;batman@batbook: ~\a\e[01;32mbatman@batbook\e[00m:\e[01;34m~\e[00m$ " 121 | - delay: 1000 122 | content: c 123 | - delay: 50 124 | content: o 125 | - delay: 50 126 | content: 'n' 127 | - delay: 50 128 | content: d 129 | - delay: 50 130 | content: a 131 | - delay: 50 132 | content: ' ' 133 | - delay: 50 134 | content: a 135 | - delay: 50 136 | content: c 137 | - delay: 50 138 | content: t 139 | - delay: 50 140 | content: i 141 | - delay: 50 142 | content: v 143 | - delay: 50 144 | content: a 145 | - delay: 50 146 | content: t 147 | - delay: 50 148 | content: e 149 | - delay: 50 150 | content: ' ' 151 | - delay: 50 152 | content: c 153 | - delay: 50 154 | content: o 155 | - delay: 50 156 | content: o 157 | - delay: 50 158 | content: k 159 | - delay: 50 160 | content: i 161 | - delay: 50 162 | content: e 163 | - delay: 50 164 | content: c 165 | - delay: 50 166 | content: u 167 | - delay: 50 168 | content: t 169 | - delay: 50 170 | content: t 171 | - delay: 50 172 | content: e 173 | - delay: 50 174 | content: r 175 | - delay: 50 176 | content: '-' 177 | - delay: 50 178 | content: e 179 | - delay: 50 180 | content: 'n' 181 | - delay: 50 182 | content: v 183 | - delay: 1000 184 | content: "\r\n" 185 | - delay: 660 186 | content: "\e]0;batman@batbook:~\e\\\e]7;file://batbook/home/batman\e\\(cookiecutter-env) \e]0;batman@batbook: ~\a\e[01;32mbatman@batbook\e[00m:\e[01;34m~\e[00m$ " 187 | - delay: 800 188 | content: c 189 | - delay: 50 190 | content: o 191 | - delay: 50 192 | content: o 193 | - delay: 50 194 | content: k 195 | - delay: 50 196 | content: i 197 | - delay: 50 198 | content: e 199 | - delay: 50 200 | content: c 201 | - delay: 50 202 | content: u 203 | - delay: 50 204 | content: t 205 | - delay: 50 206 | content: t 207 | - delay: 50 208 | content: e 209 | - delay: 50 210 | content: r 211 | - delay: 50 212 | content: ' ' 213 | - delay: 50 214 | content: h 215 | - delay: 50 216 | content: t 217 | - delay: 50 218 | content: t 219 | - delay: 50 220 | content: p 221 | - delay: 50 222 | content: s 223 | - delay: 50 224 | content: ':' 225 | - delay: 50 226 | content: / 227 | - delay: 50 228 | content: / 229 | - delay: 50 230 | content: g 231 | - delay: 50 232 | content: i 233 | - delay: 50 234 | content: t 235 | - delay: 50 236 | content: h 237 | - delay: 50 238 | content: u 239 | - delay: 50 240 | content: b 241 | - delay: 50 242 | content: . 243 | - delay: 50 244 | content: c 245 | - delay: 50 246 | content: o 247 | - delay: 50 248 | content: m 249 | - delay: 50 250 | content: / 251 | - delay: 50 252 | content: a 253 | - delay: 50 254 | content: t 255 | - delay: 50 256 | content: '-' 257 | - delay: 50 258 | content: g 259 | - delay: 50 260 | content: m 261 | - delay: 50 262 | content: b 263 | - delay: 50 264 | content: h 265 | - delay: 50 266 | content: / 267 | - delay: 50 268 | content: a 269 | - delay: 50 270 | content: t 271 | - delay: 50 272 | content: '-' 273 | - delay: 50 274 | content: p 275 | - delay: 50 276 | content: 'y' 277 | - delay: 50 278 | content: t 279 | - delay: 50 280 | content: h 281 | - delay: 50 282 | content: o 283 | - delay: 50 284 | content: 'n' 285 | - delay: 50 286 | content: '-' 287 | - delay: 50 288 | content: t 289 | - delay: 50 290 | content: e 291 | - delay: 50 292 | content: m 293 | - delay: 50 294 | content: p 295 | - delay: 50 296 | content: l 297 | - delay: 50 298 | content: a 299 | - delay: 50 300 | content: t 301 | - delay: 50 302 | content: e 303 | - delay: 1000 304 | content: "\r\n" 305 | - delay: 500 306 | content: 'full_name [Jane Doe]: ' 307 | - delay: 800 308 | content: B 309 | - delay: 50 310 | content: a 311 | - delay: 50 312 | content: t 313 | - delay: 50 314 | content: m 315 | - delay: 50 316 | content: a 317 | - delay: 50 318 | content: 'n' 319 | - delay: 500 320 | content: "\r\ncompany_name []: " 321 | - delay: 800 322 | content: W 323 | - delay: 50 324 | content: a 325 | - delay: 50 326 | content: y 327 | - delay: 50 328 | content: n 329 | - delay: 50 330 | content: e 331 | - delay: 50 332 | content: ' ' 333 | - delay: 50 334 | content: E 335 | - delay: 50 336 | content: n 337 | - delay: 50 338 | content: t 339 | - delay: 50 340 | content: e 341 | - delay: 50 342 | content: r 343 | - delay: 50 344 | content: p 345 | - delay: 50 346 | content: r 347 | - delay: 50 348 | content: i 349 | - delay: 50 350 | content: s 351 | - delay: 50 352 | content: e 353 | - delay: 50 354 | content: s 355 | - delay: 500 356 | content: "\r\nemail [contact@alexanderthamm.com]: " 357 | - delay: 800 358 | content: b 359 | - delay: 50 360 | content: r 361 | - delay: 50 362 | content: u 363 | - delay: 50 364 | content: c 365 | - delay: 50 366 | content: e 367 | - delay: 50 368 | content: . 369 | - delay: 50 370 | content: w 371 | - delay: 50 372 | content: a 373 | - delay: 50 374 | content: 'y' 375 | - delay: 50 376 | content: 'n' 377 | - delay: 50 378 | content: e 379 | - delay: 50 380 | content: '@' 381 | - delay: 50 382 | content: w 383 | - delay: 50 384 | content: e 385 | - delay: 50 386 | content: . 387 | - delay: 50 388 | content: c 389 | - delay: 50 390 | content: o 391 | - delay: 50 392 | content: m 393 | - delay: 500 394 | content: "\r\nproject_name [My Project]: " 395 | - delay: 800 396 | content: b 397 | - delay: 50 398 | content: a 399 | - delay: 50 400 | content: t 401 | - delay: 50 402 | content: b 403 | - delay: 50 404 | content: o 405 | - delay: 50 406 | content: t 407 | - delay: 500 408 | content: "\r\nproject_slug [batbot]: " 409 | - delay: 800 410 | content: "\r\n" 411 | - delay: 500 412 | content: 'module_name [batbot]: ' 413 | - delay: 800 414 | content: "\r\nproject_short_description [A short summary of the project]: " 415 | - delay: 800 416 | content: a 417 | - delay: 50 418 | content: 'n' 419 | - delay: 50 420 | content: s 421 | - delay: 50 422 | content: w 423 | - delay: 50 424 | content: e 425 | - delay: 50 426 | content: r 427 | - delay: 50 428 | content: ' ' 429 | - delay: 50 430 | content: c 431 | - delay: 50 432 | content: a 433 | - delay: 50 434 | content: l 435 | - delay: 50 436 | content: l 437 | - delay: 50 438 | content: s 439 | - delay: 50 440 | content: ' ' 441 | - delay: 50 442 | content: a 443 | - delay: 50 444 | content: 'n' 445 | - delay: 50 446 | content: d 447 | - delay: 50 448 | content: ' ' 449 | - delay: 50 450 | content: f 451 | - delay: 50 452 | content: i 453 | - delay: 50 454 | content: g 455 | - delay: 50 456 | content: h 457 | - delay: 50 458 | content: t 459 | - delay: 50 460 | content: ' ' 461 | - delay: 50 462 | content: c 463 | - delay: 50 464 | content: r 465 | - delay: 50 466 | content: i 467 | - delay: 50 468 | content: m 469 | - delay: 50 470 | content: e 471 | - delay: 50 472 | content: ' ' 473 | - delay: 50 474 | content: w 475 | - delay: 50 476 | content: h 477 | - delay: 50 478 | content: e 479 | - delay: 50 480 | content: 'n' 481 | - delay: 50 482 | content: ' ' 483 | - delay: 50 484 | content: I 485 | - delay: 50 486 | content: '''' 487 | - delay: 50 488 | content: m 489 | - delay: 50 490 | content: ' ' 491 | - delay: 50 492 | content: o 493 | - delay: 50 494 | content: u 495 | - delay: 50 496 | content: t 497 | - delay: 50 498 | content: ' ' 499 | - delay: 50 500 | content: o 501 | - delay: 50 502 | content: f 503 | - delay: 50 504 | content: ' ' 505 | - delay: 50 506 | content: o 507 | - delay: 50 508 | content: f 509 | - delay: 50 510 | content: f 511 | - delay: 50 512 | content: i 513 | - delay: 50 514 | content: c 515 | - delay: 50 516 | content: e 517 | - delay: 500 518 | content: "\r\nSelect package_manager:\r\n1 - conda\r\n2 - pip\r\nChoose from 1, 2 [1]: " 519 | - delay: 800 520 | content: '1' 521 | - delay: 500 522 | content: "\r\nSelect use_notebooks:\r\n1 - yes\r\n2 - no\r\nChoose from 1, 2 [1]: " 523 | - delay: 800 524 | content: '1' 525 | - delay: 500 526 | content: "\r\nSelect use_docker:\r\n1 - no\r\n2 - yes\r\nChoose from 1, 2 [1]: " 527 | - delay: 800 528 | content: '2' 529 | - delay: 500 530 | content: "\r\nSelect create_cli:\r\n1 - no\r\n2 - yes\r\nChoose from 1, 2 [1]: " 531 | - delay: 800 532 | content: '2' 533 | - delay: 500 534 | content: "\r\nSelect config_file:\r\n1 - yaml\r\n2 - hocon\r\n3 - none\r\nChoose from 1, 2, 3 [1]: " 535 | - delay: 800 536 | content: '1' 537 | - delay: 500 538 | content: "\r\nSelect code_formatter:\r\n1 - none\r\n2 - black\r\nChoose from 1, 2 [1]: " 539 | - delay: 800 540 | content: '2' 541 | - delay: 500 542 | content: "\r\nSelect editor_settings:\r\n1 - none\r\n2 - vscode\r\n3 - pycharm\r\nChoose from 1, 2, 3 [1]: " 543 | - delay: 800 544 | content: '2' 545 | - delay: 500 546 | content: "\r\n" 547 | - delay: 500 548 | content: "Hey Batman! Your project was successfully created at /home/batman/batbot. Have fun with it!\r\n" 549 | - delay: 5000 550 | content: ' ' 551 | #content: "\e]0;batman@batbook:~\e\\\e]7;file://batbook/home/batman\e\\(cookiecutter-env) \e]0;batman@batbook: ~\a\e[01;32mbatman@batbook\e[00m:\e[01;34m~\e[00m$ " 552 | #content: "\e]0;batman@batbook:~\e\\\e]7;file://batbook/home/batman\e\\(cookiecutter-env) \e]0;batman@batbook: ~\a\e[01;32mbatman@batbook\e[00m:\e[01;34m~\e[00m$ " 553 | # - delay: 1000 554 | #content: ' ' 555 | -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the Post-Generate Hooks for Cookiecutter. 3 | They are executed AFTER the project has been generated, so you can adjust the final 4 | contents of the project folder, based on the user configuration. More details: 5 | https://cookiecutter.readthedocs.io/en/1.7.2/advanced/hooks.html 6 | """ 7 | import os 8 | import shutil 9 | import sys 10 | from glob import glob 11 | from pathlib import Path 12 | from typing import Iterable, Optional 13 | 14 | module_dir = 'src/{{ cookiecutter.module_name }}' 15 | 16 | files_uv = { 17 | 'pyproject.toml', 18 | } 19 | files_pip = { 20 | 'requirements.txt', 21 | 'requirements-dev.txt', 22 | 'setup.py', 23 | 'tests/pytest.ini', 24 | } 25 | 26 | files_conda = { 27 | 'environment.yml', 28 | 'environment-dev.yml', 29 | 'setup.py', 30 | 'tests/pytest.ini', 31 | } 32 | 33 | files_poetry = { 34 | 'pyproject.toml', 35 | 'poetry.toml', 36 | } 37 | 38 | files_package_manager_all = files_pip | files_conda | files_poetry | files_uv 39 | 40 | files_docker_aux = { 41 | 'docker-compose.yml', 42 | '.dockerignore', 43 | } 44 | 45 | files_dockerfile_pip = { 46 | 'Dockerfile__pip', 47 | } 48 | 49 | files_dockerfile_conda = { 50 | 'Dockerfile__conda', 51 | } 52 | 53 | files_dockerfile_poetry = { 54 | 'Dockerfile__poetry', 55 | } 56 | files_dockerfile_uv = { 57 | 'Dockerfile__uv', 58 | } 59 | 60 | files_dockerfile_all = files_dockerfile_pip | files_dockerfile_conda | files_dockerfile_poetry | files_dockerfile_uv 61 | 62 | files_cli = [ 63 | f'{module_dir}/__main__.py', 64 | f'{module_dir}/main__cli.py', 65 | ] 66 | 67 | files_config_yaml = [ 68 | f'{module_dir}/util__yaml.py', 69 | 'config/config.yml', 70 | ] 71 | 72 | files_config_hocon = [ 73 | f'{module_dir}/util__hocon.py', 74 | f'{module_dir}/res/default.conf', 75 | 'config/dev.conf', 76 | 'config/prod.conf', 77 | ] 78 | 79 | files_ci_gitlab = { 80 | ".gitlab-ci.yml", 81 | } 82 | 83 | files_ci_all = files_ci_gitlab 84 | 85 | folders_editor = [ 86 | '.idea__editor', 87 | '.vscode__editor', 88 | ] 89 | 90 | 91 | def handle_package_manager(): 92 | package_manager = '{{ cookiecutter.package_manager }}' 93 | if package_manager == 'conda': 94 | _delete_files(files_package_manager_all - files_conda) 95 | elif package_manager == 'pip': 96 | _delete_files(files_package_manager_all - files_pip) 97 | elif package_manager == 'poetry': 98 | _delete_files(files_package_manager_all - files_poetry) 99 | elif package_manager == 'uv': 100 | _delete_files(files_package_manager_all - files_uv) 101 | else: 102 | print(f"Error: unsupported package manager {package_manager}") 103 | sys.exit(1) 104 | 105 | 106 | def handle_notebooks(): 107 | use_notebooks = '{{ cookiecutter.use_notebooks }}' 108 | if use_notebooks == 'no': 109 | shutil.rmtree('notebooks') 110 | 111 | 112 | def handle_docker(): 113 | use_docker = '{{ cookiecutter.use_docker }}' 114 | if use_docker == 'no': 115 | _delete_files(files_docker_aux | files_dockerfile_all) 116 | else: 117 | package_manager = '{{ cookiecutter.package_manager }}' 118 | if package_manager == "conda": 119 | _delete_files(files_dockerfile_all - files_dockerfile_conda) 120 | _rename_files("Dockerfile__conda", "__conda", "") 121 | elif package_manager == "pip": 122 | _delete_files(files_dockerfile_all - files_dockerfile_pip) 123 | _rename_files("Dockerfile__pip", "__pip", "") 124 | elif package_manager == "poetry": 125 | _delete_files(files_dockerfile_all - files_dockerfile_poetry) 126 | _rename_files("Dockerfile__poetry", "__poetry", "") 127 | elif package_manager == "uv": 128 | _delete_files(files_dockerfile_all - files_dockerfile_uv) 129 | _rename_files("Dockerfile__uv", "__uv", "") 130 | 131 | 132 | def handle_cli(): 133 | create_cli = '{{ cookiecutter.create_cli }}' 134 | if create_cli == 'yes': 135 | _delete_files([f'{module_dir}/main.py']) 136 | _rename_files('src/**/*__cli.py', '__cli', '') 137 | else: 138 | _delete_files(files_cli) 139 | 140 | 141 | def handle_config(): 142 | config_file = '{{ cookiecutter.config_file }}' 143 | if config_file == 'yaml': 144 | _delete_files(files_config_hocon) 145 | shutil.rmtree(f'{module_dir}/res') 146 | _rename_files(f'src/**/*__yaml.py', '__yaml', '') 147 | elif config_file == 'hocon': 148 | _delete_files(files_config_yaml) 149 | _rename_files(f'src/**/*__hocon.py', '__hocon', '') 150 | else: 151 | _delete_files(files_config_hocon + files_config_yaml + ['tests/test_util.py']) 152 | os.rmdir(f'{module_dir}/res') 153 | os.rmdir('config') 154 | 155 | 156 | def handle_formatter(): 157 | code_formatter = '{{ cookiecutter.code_formatter }}' 158 | if code_formatter in ['none', 'black']: 159 | # no action necessary 160 | pass 161 | else: 162 | print(f"Error: unsupported formatter {code_formatter}") 163 | sys.exit(1) 164 | 165 | 166 | def handle_editor_settings(): 167 | editor_settings = '{{ cookiecutter.editor_settings }}' 168 | if editor_settings == 'vscode': 169 | _rename_files('.vscode__editor', '__editor', '') 170 | _delete_folders(folders_editor, exclude='.vscode') 171 | elif editor_settings == 'pycharm': 172 | _rename_files('.idea__editor', '__editor', '') 173 | _delete_folders(folders_editor, exclude='.idea') 174 | elif editor_settings == 'none': 175 | _delete_folders(folders_editor) 176 | else: 177 | print(f"Error: unsupported editor {editor_settings}") 178 | sys.exit(1) 179 | 180 | 181 | def handle_ci(): 182 | ci_pipeline = '{{ cookiecutter.ci_pipeline }}' 183 | if ci_pipeline == "gitlab": 184 | _delete_files(files_ci_all - files_ci_gitlab) 185 | elif ci_pipeline == 'none': 186 | _delete_files(files_ci_all) 187 | 188 | 189 | def print_success(): 190 | full_name = '{{ cookiecutter.full_name }}' 191 | print(f"Hey {full_name}! Your project was successfully created at {os.getcwd()}. " 192 | f"Have fun with it!") 193 | 194 | 195 | def _rename_files(file_pattern, old, new): 196 | for file in glob(file_pattern, recursive=True): 197 | path = Path(file) 198 | path.rename(path.with_name(path.name.replace(old, new))) 199 | 200 | 201 | def _delete_files(files: Iterable[str], exclude: Optional[str] = None): 202 | try: 203 | for file in files: 204 | if file != exclude: 205 | os.remove(file) 206 | except OSError as e: 207 | print(f"Error: failed to remove files - {e}") 208 | sys.exit(1) 209 | 210 | 211 | def _delete_folders(folders: Iterable[str], exclude: Optional[str] = None): 212 | for folder in folders: 213 | if folder != exclude: 214 | shutil.rmtree(folder, ignore_errors=True) 215 | 216 | 217 | if __name__ == '__main__': 218 | handle_package_manager() 219 | handle_cli() 220 | handle_notebooks() 221 | handle_docker() 222 | handle_config() 223 | handle_formatter() 224 | handle_editor_settings() 225 | handle_ci() 226 | print_success() 227 | -------------------------------------------------------------------------------- /hooks/pre_gen_project.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the Pre-Generate Hooks for Cookiecutter. 3 | They are executed AFTER the user entered their project config, 4 | but BEFORE the project is actually generated. More details: 5 | https://cookiecutter.readthedocs.io/en/1.7.2/advanced/hooks.html 6 | 7 | In this script we execute environment checks and run input validation. 8 | Because the environment check needs to work with old Python versions (e.g. Python 2.7), 9 | we need to avoid modern syntax features in this file, like f-strings and type hints. 10 | """ 11 | import platform 12 | import re 13 | import sys 14 | from packaging.version import Version 15 | import warnings 16 | 17 | import cookiecutter 18 | 19 | 20 | with warnings.catch_warnings(): 21 | warnings.simplefilter("ignore", category=DeprecationWarning) 22 | # check Python version (3.9 or higher) 23 | if Version(platform.python_version()) < Version("3.9.0"): 24 | print(f"ERROR: You are using Python {platform.python_version()},", 25 | "but Python 3.9 or higher is required to use this template") 26 | sys.exit(1) 27 | # check cookiecutter version (1.7.2 or higher) 28 | if Version(cookiecutter.__version__) < Version('1.7.2'): 29 | print(f"ERROR: You are using cookiecutter {cookiecutter.__version__}" 30 | "but cookiecutter 1.7.2 or higher is required to use this template") 31 | sys.exit(1) 32 | 33 | # check the slug and module name 34 | SLUG_REGEX = r'^[a-zA-Z][-a-zA-Z0-9]+$' 35 | MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$' 36 | 37 | project_slug = '{{ cookiecutter.project_slug }}' 38 | module_name = '{{ cookiecutter.module_name }}' 39 | 40 | invalid_inputs = list() 41 | if not re.match(SLUG_REGEX, project_slug): 42 | print(f"ERROR: {project_slug} is not a valid slug! " 43 | "It may only consist of numbers and letters of the english alphabet, " 44 | "begin with a letter, and must use dashes instead of whitespace.") 45 | invalid_inputs.append("project_slug") 46 | 47 | if not re.match(MODULE_REGEX, module_name): 48 | print(f"ERROR: {module_name} is not a valid Python module name! " 49 | "See https://www.python.org/dev/peps/pep-0008/#package-and-module-names " 50 | "for naming standards.") 51 | invalid_inputs.append("module_name") 52 | 53 | if invalid_inputs: 54 | print("\nYou have entered invalid configuration values for:", 55 | ", ".join(invalid_inputs)) 56 | print("\nPlease fix your inputs in ~/.cookiecutter_replay/at-python-template.json") 57 | print("After that, rerun the template with: cookiecutter . --replay\n") 58 | sys.exit(1) 59 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "arrow" 5 | version = "1.3.0" 6 | description = "Better dates & times for Python" 7 | optional = false 8 | python-versions = ">=3.8" 9 | groups = ["main"] 10 | files = [ 11 | {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, 12 | {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, 13 | ] 14 | 15 | [package.dependencies] 16 | python-dateutil = ">=2.7.0" 17 | types-python-dateutil = ">=2.8.10" 18 | 19 | [package.extras] 20 | doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] 21 | test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] 22 | 23 | [[package]] 24 | name = "binaryornot" 25 | version = "0.4.4" 26 | description = "Ultra-lightweight pure Python package to check if a file is binary or text." 27 | optional = false 28 | python-versions = "*" 29 | groups = ["main"] 30 | files = [ 31 | {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, 32 | {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, 33 | ] 34 | 35 | [package.dependencies] 36 | chardet = ">=3.0.2" 37 | 38 | [[package]] 39 | name = "certifi" 40 | version = "2025.4.26" 41 | description = "Python package for providing Mozilla's CA Bundle." 42 | optional = false 43 | python-versions = ">=3.6" 44 | groups = ["main"] 45 | files = [ 46 | {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, 47 | {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, 48 | ] 49 | 50 | [[package]] 51 | name = "cfgv" 52 | version = "3.4.0" 53 | description = "Validate configuration and produce human readable error messages." 54 | optional = false 55 | python-versions = ">=3.8" 56 | groups = ["main"] 57 | files = [ 58 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 59 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 60 | ] 61 | 62 | [[package]] 63 | name = "chardet" 64 | version = "5.2.0" 65 | description = "Universal encoding detector for Python 3" 66 | optional = false 67 | python-versions = ">=3.7" 68 | groups = ["main"] 69 | files = [ 70 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, 71 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, 72 | ] 73 | 74 | [[package]] 75 | name = "charset-normalizer" 76 | version = "3.4.2" 77 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 78 | optional = false 79 | python-versions = ">=3.7" 80 | groups = ["main"] 81 | files = [ 82 | {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, 83 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, 84 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, 85 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, 86 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, 87 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, 88 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, 89 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, 90 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, 91 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, 92 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, 93 | {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, 94 | {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, 95 | {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, 96 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, 97 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, 98 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, 99 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, 100 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, 101 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, 102 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, 103 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, 104 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, 105 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, 106 | {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, 107 | {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, 108 | {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, 109 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, 110 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, 111 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, 112 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, 113 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, 114 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, 115 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, 116 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, 117 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, 118 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, 119 | {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, 120 | {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, 121 | {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, 122 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, 123 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, 124 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, 125 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, 126 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, 127 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, 128 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, 129 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, 130 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, 131 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, 132 | {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, 133 | {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, 134 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, 135 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, 136 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, 137 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, 138 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, 139 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, 140 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, 141 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, 142 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, 143 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, 144 | {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, 145 | {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, 146 | {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, 147 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, 148 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, 149 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, 150 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, 151 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, 152 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, 153 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, 154 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, 155 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, 156 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, 157 | {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, 158 | {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, 159 | {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, 160 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, 161 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, 162 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, 163 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, 164 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, 165 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, 166 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, 167 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, 168 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, 169 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, 170 | {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, 171 | {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, 172 | {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, 173 | {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, 174 | ] 175 | 176 | [[package]] 177 | name = "click" 178 | version = "8.1.8" 179 | description = "Composable command line interface toolkit" 180 | optional = false 181 | python-versions = ">=3.7" 182 | groups = ["main"] 183 | files = [ 184 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 185 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 186 | ] 187 | 188 | [package.dependencies] 189 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 190 | 191 | [[package]] 192 | name = "colorama" 193 | version = "0.4.6" 194 | description = "Cross-platform colored terminal text." 195 | optional = false 196 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 197 | groups = ["main", "dev"] 198 | files = [ 199 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 200 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 201 | ] 202 | markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} 203 | 204 | [[package]] 205 | name = "cookiecutter" 206 | version = "2.6.0" 207 | description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." 208 | optional = false 209 | python-versions = ">=3.7" 210 | groups = ["main"] 211 | files = [ 212 | {file = "cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d"}, 213 | {file = "cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c"}, 214 | ] 215 | 216 | [package.dependencies] 217 | arrow = "*" 218 | binaryornot = ">=0.4.4" 219 | click = ">=7.0,<9.0.0" 220 | Jinja2 = ">=2.7,<4.0.0" 221 | python-slugify = ">=4.0.0" 222 | pyyaml = ">=5.3.1" 223 | requests = ">=2.23.0" 224 | rich = "*" 225 | 226 | [[package]] 227 | name = "coverage" 228 | version = "7.8.2" 229 | description = "Code coverage measurement for Python" 230 | optional = false 231 | python-versions = ">=3.9" 232 | groups = ["dev"] 233 | files = [ 234 | {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, 235 | {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, 236 | {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, 237 | {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, 238 | {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, 239 | {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, 240 | {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, 241 | {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, 242 | {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, 243 | {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, 244 | {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, 245 | {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, 246 | {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, 247 | {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, 248 | {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, 249 | {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, 250 | {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, 251 | {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, 252 | {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, 253 | {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, 254 | {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, 255 | {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, 256 | {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, 257 | {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, 258 | {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, 259 | {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, 260 | {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, 261 | {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, 262 | {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, 263 | {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, 264 | {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, 265 | {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, 266 | {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, 267 | {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, 268 | {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, 269 | {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, 270 | {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, 271 | {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, 272 | {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, 273 | {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, 274 | {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, 275 | {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, 276 | {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, 277 | {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, 278 | {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, 279 | {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, 280 | {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, 281 | {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, 282 | {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, 283 | {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, 284 | {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, 285 | {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, 286 | {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, 287 | {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, 288 | {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, 289 | {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, 290 | {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, 291 | {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, 292 | {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, 293 | {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, 294 | {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, 295 | {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, 296 | {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, 297 | {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, 298 | {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, 299 | {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, 300 | {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, 301 | ] 302 | 303 | [package.dependencies] 304 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 305 | 306 | [package.extras] 307 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 308 | 309 | [[package]] 310 | name = "distlib" 311 | version = "0.3.9" 312 | description = "Distribution utilities" 313 | optional = false 314 | python-versions = "*" 315 | groups = ["main"] 316 | files = [ 317 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 318 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 319 | ] 320 | 321 | [[package]] 322 | name = "exceptiongroup" 323 | version = "1.3.0" 324 | description = "Backport of PEP 654 (exception groups)" 325 | optional = false 326 | python-versions = ">=3.7" 327 | groups = ["main", "dev"] 328 | markers = "python_version < \"3.11\"" 329 | files = [ 330 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 331 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 332 | ] 333 | 334 | [package.dependencies] 335 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 336 | 337 | [package.extras] 338 | test = ["pytest (>=6)"] 339 | 340 | [[package]] 341 | name = "filelock" 342 | version = "3.18.0" 343 | description = "A platform independent file lock." 344 | optional = false 345 | python-versions = ">=3.9" 346 | groups = ["main"] 347 | files = [ 348 | {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, 349 | {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, 350 | ] 351 | 352 | [package.extras] 353 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 354 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] 355 | typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] 356 | 357 | [[package]] 358 | name = "identify" 359 | version = "2.6.12" 360 | description = "File identification library for Python" 361 | optional = false 362 | python-versions = ">=3.9" 363 | groups = ["main"] 364 | files = [ 365 | {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}, 366 | {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}, 367 | ] 368 | 369 | [package.extras] 370 | license = ["ukkonen"] 371 | 372 | [[package]] 373 | name = "idna" 374 | version = "3.10" 375 | description = "Internationalized Domain Names in Applications (IDNA)" 376 | optional = false 377 | python-versions = ">=3.6" 378 | groups = ["main"] 379 | files = [ 380 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 381 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 382 | ] 383 | 384 | [package.extras] 385 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 386 | 387 | [[package]] 388 | name = "iniconfig" 389 | version = "2.1.0" 390 | description = "brain-dead simple config-ini parsing" 391 | optional = false 392 | python-versions = ">=3.8" 393 | groups = ["main", "dev"] 394 | files = [ 395 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 396 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 397 | ] 398 | 399 | [[package]] 400 | name = "jinja2" 401 | version = "3.1.6" 402 | description = "A very fast and expressive template engine." 403 | optional = false 404 | python-versions = ">=3.7" 405 | groups = ["main"] 406 | files = [ 407 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 408 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 409 | ] 410 | 411 | [package.dependencies] 412 | MarkupSafe = ">=2.0" 413 | 414 | [package.extras] 415 | i18n = ["Babel (>=2.7)"] 416 | 417 | [[package]] 418 | name = "markdown-it-py" 419 | version = "3.0.0" 420 | description = "Python port of markdown-it. Markdown parsing, done right!" 421 | optional = false 422 | python-versions = ">=3.8" 423 | groups = ["main"] 424 | files = [ 425 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 426 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 427 | ] 428 | 429 | [package.dependencies] 430 | mdurl = ">=0.1,<1.0" 431 | 432 | [package.extras] 433 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 434 | code-style = ["pre-commit (>=3.0,<4.0)"] 435 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 436 | linkify = ["linkify-it-py (>=1,<3)"] 437 | plugins = ["mdit-py-plugins"] 438 | profiling = ["gprof2dot"] 439 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 440 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 441 | 442 | [[package]] 443 | name = "markupsafe" 444 | version = "3.0.2" 445 | description = "Safely add untrusted strings to HTML/XML markup." 446 | optional = false 447 | python-versions = ">=3.9" 448 | groups = ["main"] 449 | files = [ 450 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 451 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 452 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 453 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 454 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 455 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 456 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 457 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 458 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 459 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 460 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 461 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 462 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 463 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 464 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 465 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 466 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 467 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 468 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 469 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 470 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 471 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 472 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 473 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 474 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 475 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 476 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 477 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 478 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 479 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 480 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 481 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 482 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 483 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 484 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 485 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 486 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 487 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 488 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 489 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 490 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 491 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 492 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 493 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 494 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 495 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 496 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 497 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 498 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 499 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 500 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 501 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 502 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 503 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 504 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 505 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 506 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 507 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 508 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 509 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 510 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 511 | ] 512 | 513 | [[package]] 514 | name = "mdurl" 515 | version = "0.1.2" 516 | description = "Markdown URL utilities" 517 | optional = false 518 | python-versions = ">=3.7" 519 | groups = ["main"] 520 | files = [ 521 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 522 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 523 | ] 524 | 525 | [[package]] 526 | name = "nodeenv" 527 | version = "1.9.1" 528 | description = "Node.js virtual environment builder" 529 | optional = false 530 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 531 | groups = ["main"] 532 | files = [ 533 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 534 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 535 | ] 536 | 537 | [[package]] 538 | name = "packaging" 539 | version = "25.0" 540 | description = "Core utilities for Python packages" 541 | optional = false 542 | python-versions = ">=3.8" 543 | groups = ["main", "dev"] 544 | files = [ 545 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 546 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 547 | ] 548 | 549 | [[package]] 550 | name = "platformdirs" 551 | version = "4.3.8" 552 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 553 | optional = false 554 | python-versions = ">=3.9" 555 | groups = ["main"] 556 | files = [ 557 | {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, 558 | {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, 559 | ] 560 | 561 | [package.extras] 562 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 563 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 564 | type = ["mypy (>=1.14.1)"] 565 | 566 | [[package]] 567 | name = "pluggy" 568 | version = "1.6.0" 569 | description = "plugin and hook calling mechanisms for python" 570 | optional = false 571 | python-versions = ">=3.9" 572 | groups = ["main", "dev"] 573 | files = [ 574 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 575 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 576 | ] 577 | 578 | [package.extras] 579 | dev = ["pre-commit", "tox"] 580 | testing = ["coverage", "pytest", "pytest-benchmark"] 581 | 582 | [[package]] 583 | name = "pre-commit" 584 | version = "4.2.0" 585 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 586 | optional = false 587 | python-versions = ">=3.9" 588 | groups = ["main"] 589 | files = [ 590 | {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, 591 | {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, 592 | ] 593 | 594 | [package.dependencies] 595 | cfgv = ">=2.0.0" 596 | identify = ">=1.0.0" 597 | nodeenv = ">=0.11.1" 598 | pyyaml = ">=5.1" 599 | virtualenv = ">=20.10.0" 600 | 601 | [[package]] 602 | name = "pygments" 603 | version = "2.19.1" 604 | description = "Pygments is a syntax highlighting package written in Python." 605 | optional = false 606 | python-versions = ">=3.8" 607 | groups = ["main", "dev"] 608 | files = [ 609 | {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, 610 | {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, 611 | ] 612 | 613 | [package.extras] 614 | windows-terminal = ["colorama (>=0.4.6)"] 615 | 616 | [[package]] 617 | name = "pyhocon" 618 | version = "0.3.61" 619 | description = "HOCON parser for Python" 620 | optional = false 621 | python-versions = "*" 622 | groups = ["main"] 623 | files = [ 624 | {file = "pyhocon-0.3.61-py3-none-any.whl", hash = "sha256:73d0f064af9a7d454949c5557284ce1d716cfd8e1383ecc90095fc575d278df0"}, 625 | ] 626 | 627 | [package.dependencies] 628 | pyparsing = {version = ">=2,<4", markers = "python_version >= \"3.0\""} 629 | 630 | [package.extras] 631 | duration = ["python-dateutil (>=2.8.0)"] 632 | 633 | [[package]] 634 | name = "pyparsing" 635 | version = "3.2.3" 636 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 637 | optional = false 638 | python-versions = ">=3.9" 639 | groups = ["main"] 640 | files = [ 641 | {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, 642 | {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, 643 | ] 644 | 645 | [package.extras] 646 | diagrams = ["jinja2", "railroad-diagrams"] 647 | 648 | [[package]] 649 | name = "pytest" 650 | version = "8.4.0" 651 | description = "pytest: simple powerful testing with Python" 652 | optional = false 653 | python-versions = ">=3.9" 654 | groups = ["main", "dev"] 655 | files = [ 656 | {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, 657 | {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, 658 | ] 659 | 660 | [package.dependencies] 661 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 662 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 663 | iniconfig = ">=1" 664 | packaging = ">=20" 665 | pluggy = ">=1.5,<2" 666 | pygments = ">=2.7.2" 667 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 668 | 669 | [package.extras] 670 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 671 | 672 | [[package]] 673 | name = "pytest-cov" 674 | version = "6.1.1" 675 | description = "Pytest plugin for measuring coverage." 676 | optional = false 677 | python-versions = ">=3.9" 678 | groups = ["dev"] 679 | files = [ 680 | {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, 681 | {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, 682 | ] 683 | 684 | [package.dependencies] 685 | coverage = {version = ">=7.5", extras = ["toml"]} 686 | pytest = ">=4.6" 687 | 688 | [package.extras] 689 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 690 | 691 | [[package]] 692 | name = "pytest-mock" 693 | version = "3.14.1" 694 | description = "Thin-wrapper around the mock package for easier use with pytest" 695 | optional = false 696 | python-versions = ">=3.8" 697 | groups = ["main"] 698 | files = [ 699 | {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, 700 | {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, 701 | ] 702 | 703 | [package.dependencies] 704 | pytest = ">=6.2.5" 705 | 706 | [package.extras] 707 | dev = ["pre-commit", "pytest-asyncio", "tox"] 708 | 709 | [[package]] 710 | name = "python-dateutil" 711 | version = "2.9.0.post0" 712 | description = "Extensions to the standard Python datetime module" 713 | optional = false 714 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 715 | groups = ["main"] 716 | files = [ 717 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 718 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 719 | ] 720 | 721 | [package.dependencies] 722 | six = ">=1.5" 723 | 724 | [[package]] 725 | name = "python-slugify" 726 | version = "8.0.4" 727 | description = "A Python slugify application that also handles Unicode" 728 | optional = false 729 | python-versions = ">=3.7" 730 | groups = ["main"] 731 | files = [ 732 | {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, 733 | {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, 734 | ] 735 | 736 | [package.dependencies] 737 | text-unidecode = ">=1.3" 738 | 739 | [package.extras] 740 | unidecode = ["Unidecode (>=1.1.1)"] 741 | 742 | [[package]] 743 | name = "pyyaml" 744 | version = "6.0.2" 745 | description = "YAML parser and emitter for Python" 746 | optional = false 747 | python-versions = ">=3.8" 748 | groups = ["main"] 749 | files = [ 750 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 751 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 752 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 753 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 754 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 755 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 756 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 757 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 758 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 759 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 760 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 761 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 762 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 763 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 764 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 765 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 766 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 767 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 768 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 769 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 770 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 771 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 772 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 773 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 774 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 775 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 776 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 777 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 778 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 779 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 780 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 781 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 782 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 783 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 784 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 785 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 786 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 787 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 788 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 789 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 790 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 791 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 792 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 793 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 794 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 795 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 796 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 797 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 798 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 799 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 800 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 801 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 802 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 803 | ] 804 | 805 | [[package]] 806 | name = "requests" 807 | version = "2.32.3" 808 | description = "Python HTTP for Humans." 809 | optional = false 810 | python-versions = ">=3.8" 811 | groups = ["main"] 812 | files = [ 813 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 814 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 815 | ] 816 | 817 | [package.dependencies] 818 | certifi = ">=2017.4.17" 819 | charset-normalizer = ">=2,<4" 820 | idna = ">=2.5,<4" 821 | urllib3 = ">=1.21.1,<3" 822 | 823 | [package.extras] 824 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 825 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 826 | 827 | [[package]] 828 | name = "rich" 829 | version = "14.0.0" 830 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 831 | optional = false 832 | python-versions = ">=3.8.0" 833 | groups = ["main"] 834 | files = [ 835 | {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, 836 | {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, 837 | ] 838 | 839 | [package.dependencies] 840 | markdown-it-py = ">=2.2.0" 841 | pygments = ">=2.13.0,<3.0.0" 842 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} 843 | 844 | [package.extras] 845 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 846 | 847 | [[package]] 848 | name = "setuptools" 849 | version = "80.9.0" 850 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 851 | optional = false 852 | python-versions = ">=3.9" 853 | groups = ["main"] 854 | files = [ 855 | {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, 856 | {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, 857 | ] 858 | 859 | [package.extras] 860 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] 861 | core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] 862 | cover = ["pytest-cov"] 863 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 864 | enabler = ["pytest-enabler (>=2.2)"] 865 | test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] 866 | type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] 867 | 868 | [[package]] 869 | name = "shellingham" 870 | version = "1.5.4" 871 | description = "Tool to Detect Surrounding Shell" 872 | optional = false 873 | python-versions = ">=3.7" 874 | groups = ["main"] 875 | files = [ 876 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 877 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 878 | ] 879 | 880 | [[package]] 881 | name = "six" 882 | version = "1.17.0" 883 | description = "Python 2 and 3 compatibility utilities" 884 | optional = false 885 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 886 | groups = ["main"] 887 | files = [ 888 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 889 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 890 | ] 891 | 892 | [[package]] 893 | name = "text-unidecode" 894 | version = "1.3" 895 | description = "The most basic Text::Unidecode port" 896 | optional = false 897 | python-versions = "*" 898 | groups = ["main"] 899 | files = [ 900 | {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, 901 | {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, 902 | ] 903 | 904 | [[package]] 905 | name = "tomli" 906 | version = "2.2.1" 907 | description = "A lil' TOML parser" 908 | optional = false 909 | python-versions = ">=3.8" 910 | groups = ["main", "dev"] 911 | files = [ 912 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 913 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 914 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 915 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 916 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 917 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 918 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 919 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 920 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 921 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 922 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 923 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 924 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 925 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 926 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 927 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 928 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 929 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 930 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 931 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 932 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 933 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 934 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 935 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 936 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 937 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 938 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 939 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 940 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 941 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 942 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 943 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 944 | ] 945 | markers = {main = "python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""} 946 | 947 | [[package]] 948 | name = "typer" 949 | version = "0.16.0" 950 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 951 | optional = false 952 | python-versions = ">=3.7" 953 | groups = ["main"] 954 | files = [ 955 | {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, 956 | {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, 957 | ] 958 | 959 | [package.dependencies] 960 | click = ">=8.0.0" 961 | rich = ">=10.11.0" 962 | shellingham = ">=1.3.0" 963 | typing-extensions = ">=3.7.4.3" 964 | 965 | [[package]] 966 | name = "types-python-dateutil" 967 | version = "2.9.0.20250516" 968 | description = "Typing stubs for python-dateutil" 969 | optional = false 970 | python-versions = ">=3.9" 971 | groups = ["main"] 972 | files = [ 973 | {file = "types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93"}, 974 | {file = "types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5"}, 975 | ] 976 | 977 | [[package]] 978 | name = "typing-extensions" 979 | version = "4.14.0" 980 | description = "Backported and Experimental Type Hints for Python 3.9+" 981 | optional = false 982 | python-versions = ">=3.9" 983 | groups = ["main", "dev"] 984 | files = [ 985 | {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, 986 | {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, 987 | ] 988 | markers = {dev = "python_version < \"3.11\""} 989 | 990 | [[package]] 991 | name = "urllib3" 992 | version = "2.4.0" 993 | description = "HTTP library with thread-safe connection pooling, file post, and more." 994 | optional = false 995 | python-versions = ">=3.9" 996 | groups = ["main"] 997 | files = [ 998 | {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, 999 | {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, 1000 | ] 1001 | 1002 | [package.extras] 1003 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 1004 | h2 = ["h2 (>=4,<5)"] 1005 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1006 | zstd = ["zstandard (>=0.18.0)"] 1007 | 1008 | [[package]] 1009 | name = "virtualenv" 1010 | version = "20.31.2" 1011 | description = "Virtual Python Environment builder" 1012 | optional = false 1013 | python-versions = ">=3.8" 1014 | groups = ["main"] 1015 | files = [ 1016 | {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, 1017 | {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, 1018 | ] 1019 | 1020 | [package.dependencies] 1021 | distlib = ">=0.3.7,<1" 1022 | filelock = ">=3.12.2,<4" 1023 | platformdirs = ">=3.9.1,<5" 1024 | 1025 | [package.extras] 1026 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1027 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 1028 | 1029 | [metadata] 1030 | lock-version = "2.1" 1031 | python-versions = "^3.9" 1032 | content-hash = "ccef86db2ae5622f80b09c6275fb89a11700d783b113029b2e67582cefcfead5" 1033 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "at-python-template" 3 | version = "1.2.1" 4 | description = "This is the official Python Project Template of Alexander Thamm GmbH (AT)" 5 | authors = [ 6 | "Christian Baur ", 7 | "Honza Bílek ", 8 | # "Sebastian Straub ", 9 | # "Steffen Bunzel ", 10 | # "Hans Rauer ", 11 | # "Simon Weiß ", 12 | ] 13 | license = "Apache License, Version 2.0" 14 | readme = "README.md" 15 | packages = [{ include = "{{cookiecutter.module_name}}", from = "{{cookiecutter.project_slug}}/src" }] 16 | 17 | [tool.poetry.dependencies] 18 | python = "^3.9" 19 | cookiecutter = "^2.3" 20 | pre-commit = "^4.1.0" 21 | pytest-mock = "^3.12" 22 | pyhocon = "^0.3.60" 23 | pyyaml = "^6.0" 24 | typer = "^0.16.0" 25 | setuptools = "^80.9.0" 26 | 27 | [tool.poetry.group.dev.dependencies] 28 | pytest = "^8.4.0" 29 | pytest-cov = "^6.0.0" 30 | 31 | [build-system] 32 | requires = ["poetry-core"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "enabled": true, 4 | "labels": [ 5 | "renovate", 6 | "dependencies" 7 | ], 8 | "extends": [ 9 | "config:recommended" 10 | ], 11 | "dependencyDashboard": true, 12 | "pre-commit": { 13 | "enabled": true 14 | }, 15 | "branchPrefix": "feature/renovate-configuration/" 16 | } 17 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/at-gmbh/at-python-template/f167921eca9cf2bbac387a5890a2f93ca5304826/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_options.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from .util import assert_file_contains, check_project 4 | 5 | 6 | def test_base(): 7 | check_project(run_pytest=True) 8 | 9 | 10 | def test_pip(): 11 | check_project( 12 | settings={'package_manager': 'pip'}, 13 | files_existent=['requirements.txt', 'requirements-dev.txt', 'setup.py'], 14 | files_non_existent=['environment.yml', 'environment-dev.yml']) 15 | 16 | 17 | def test_conda(): 18 | check_project( 19 | settings={'package_manager': 'conda'}, 20 | files_existent=['environment.yml', 'environment-dev.yml', 'setup.py']) 21 | 22 | 23 | def test_poetry(): 24 | check_project( 25 | settings={'package_manager': 'poetry'}, 26 | files_existent=['pyproject.toml'], 27 | files_non_existent=['environment.yml, environment-dev.yml', 'requirements.txt', 'requirements-dev.txt', 'setup.py'] 28 | ) 29 | 30 | 31 | def test_notebooks_yes(): 32 | check_project( 33 | settings={'use_notebooks': 'yes'}, 34 | files_existent=['notebooks']) 35 | 36 | 37 | def test_notebooks_no(): 38 | check_project( 39 | settings={'use_notebooks': 'no'}, 40 | files_non_existent=['notebooks']) 41 | 42 | 43 | def test_docker_pip(): 44 | check_project( 45 | settings={'use_docker': 'yes', 'package_manager': 'pip',}, 46 | files_existent=['Dockerfile', 'docker-compose.yml', '.dockerignore']) 47 | 48 | 49 | def test_docker_conda(): 50 | check_project( 51 | settings={'use_docker': 'yes', 'package_manager': 'conda',}, 52 | files_existent=['Dockerfile', 'docker-compose.yml', '.dockerignore']) 53 | 54 | 55 | def test_docker_poetry(): 56 | check_project( 57 | settings={'use_docker': 'yes', 'package_manager': 'poetry',}, 58 | files_existent=['Dockerfile', 'docker-compose.yml', '.dockerignore'] 59 | ) 60 | 61 | 62 | def test_docker_no(): 63 | check_project( 64 | settings={'use_docker': 'no'}, 65 | files_non_existent=['Dockerfile', 'docker-compose.yml', '.dockerignore']) 66 | 67 | 68 | def test_cli_yes(): 69 | check_project( 70 | settings={'create_cli': 'yes'}, 71 | files_existent=['src/{module_name}/main.py', 'src/{module_name}/__main__.py'], 72 | test_cli=True) 73 | 74 | 75 | def test_cli_no(): 76 | check_project( 77 | settings={'create_cli': 'no'}, 78 | files_non_existent=['src/{module_name}/__main__.py'], 79 | test_cli=False) 80 | 81 | 82 | def test_config_hocon(): 83 | check_project( 84 | settings={'config_file': 'hocon', 'create_cli': 'yes'}, 85 | files_existent=['src/{module_name}/util.py', 'src/{module_name}/res/default.conf', 86 | 'config/dev.conf', 'config/prod.conf'], 87 | files_non_existent=['config/config.yml'], 88 | test_cli=True, run_pytest=True) 89 | 90 | 91 | def test_config_yaml(): 92 | check_project( 93 | settings={'config_file': 'yaml'}, 94 | files_existent=['src/{module_name}/util.py', 'config/config.yml'], 95 | files_non_existent=['config/dev.conf', 'config/prod.conf', 'src/{module_name}/res/default.conf'], 96 | run_pytest=True) 97 | 98 | 99 | def test_config_none(): 100 | check_project( 101 | settings={'config_file': 'none'}, 102 | files_non_existent=['config', 'src/{module_name}/util.py', 'tests/util.py', 103 | 'src/{module_name}/res']) 104 | 105 | 106 | def test_formatter_black_pip(): 107 | def check_black(project_dir: Path): 108 | assert_file_contains(project_dir / '.pre-commit-config.yaml', contains='psf/black') 109 | assert_file_contains(project_dir / 'requirements-dev.txt', contains='black') 110 | 111 | check_project( 112 | settings={'code_formatter': 'black', 'package_manager': 'pip'}, 113 | fun=check_black) 114 | 115 | 116 | def test_formatter_black_conda(): 117 | def check_black(project_dir: Path): 118 | assert_file_contains(project_dir / '.pre-commit-config.yaml', contains='psf/black') 119 | assert_file_contains(project_dir / 'environment-dev.yml', contains='pip:') 120 | assert_file_contains(project_dir / 'environment-dev.yml', contains='black>=') 121 | 122 | check_project( 123 | settings={'code_formatter': 'black', 'package_manager': 'conda'}, 124 | fun=check_black) 125 | 126 | 127 | 128 | def test_formatter_black_poetry(): 129 | def check_black(project_dir: Path): 130 | assert_file_contains(project_dir / '.pre-commit-config.yaml', contains='psf/black') 131 | assert_file_contains(project_dir / 'pyproject.toml', contains='black') 132 | 133 | check_project( 134 | settings={'code_formatter': 'black', 'package_manager': 'poetry'}, 135 | fun=check_black 136 | ) 137 | 138 | 139 | def test_formatter_none(): 140 | def check_white(project_dir: Path): 141 | assert_file_contains(project_dir / '.pre-commit-config.yaml', not_contains='psf/black') 142 | assert_file_contains(project_dir / 'environment-dev.yml', not_contains='black=') 143 | 144 | check_project( 145 | settings={'code_formatter': 'none'}, 146 | fun=check_white) 147 | 148 | 149 | def test_editor_pycharm(): 150 | check_project( 151 | settings={'editor_settings': 'pycharm'}, 152 | files_existent=['.idea'], 153 | files_non_existent=['.vscode']) 154 | 155 | 156 | def test_editor_vscode(): 157 | check_project( 158 | settings={'editor_settings': 'vscode'}, 159 | files_existent=['.vscode/settings.json'], 160 | files_non_existent=['.idea']) 161 | 162 | 163 | def test_editor_none(): 164 | check_project( 165 | settings={'editor_settings': 'none'}, 166 | files_non_existent=['.idea', '.vscode']) 167 | 168 | 169 | def test_random_combination(): 170 | check_project( 171 | settings={ 172 | 'package_manager': 'pip', 173 | 'use_notebooks': 'no', 174 | 'config_file': 'hocon', 175 | 'use_docker': 'yes', 176 | 'create_cli': 'yes', 177 | 'code_formatter': 'none', 178 | 'editor_settings': 'pycharm', 179 | }, 180 | test_cli=True, run_pytest=True) 181 | 182 | 183 | def test_poetry_regression(): 184 | """ 185 | Regression test for the bug: 186 | Template test failure after creating template with package_manager=poetry 187 | """ 188 | check_project( 189 | settings={ 190 | "package_manager": "poetry", 191 | "use_notebooks": "no", 192 | "config_file": "none", 193 | "use_docker": "no", 194 | "create_cli": "no", 195 | "code_formatter": "none", 196 | "editor_settings": "none", 197 | }, 198 | test_cli=False, 199 | run_pytest=True, 200 | ) 201 | 202 | def test_gitlab_pip(): 203 | check_project( 204 | settings={ 205 | "package_manager": "pip", 206 | "ci_pipeline": "gitlab" 207 | }, 208 | files_existent=[".gitlab-ci.yml"] 209 | ) 210 | 211 | def test_gitlab_conda(): 212 | check_project( 213 | settings={ 214 | "package_manager": "conda", 215 | "ci_pipeline": "gitlab" 216 | }, 217 | files_existent=[".gitlab-ci.yml"] 218 | ) 219 | 220 | def test_gitlab_poetry(): 221 | check_project( 222 | settings={ 223 | "package_manager": "poetry", 224 | "ci_pipeline": "gitlab" 225 | }, 226 | files_existent=[".gitlab-ci.yml"] 227 | ) 228 | 229 | def test_no_ci_pipeline(): 230 | check_project( 231 | settings={ 232 | "ci_pipeline": "none" 233 | }, 234 | files_non_existent=[".gitlab-ci.yml"] 235 | ) 236 | -------------------------------------------------------------------------------- /tests/test_validation.py: -------------------------------------------------------------------------------- 1 | from runpy import run_path 2 | from typing import Any, Callable 3 | 4 | import pytest 5 | from cookiecutter.exceptions import CookiecutterException 6 | from pytest_mock import MockerFixture 7 | 8 | from .util import check_project 9 | 10 | 11 | def test_module_name(): 12 | with pytest.raises(CookiecutterException): 13 | check_project(settings={'module_name': "don't you tell *me* how to name *my* modules!"}) 14 | 15 | 16 | def test_project_slug(): 17 | with pytest.raises(CookiecutterException): 18 | check_project(settings={'project_slug': "I am afraid of snails"}) 19 | 20 | 21 | def test_editor(): 22 | check_project(settings={'editor_settings': "pycharm"}) 23 | 24 | 25 | def test_fail_python_3_8(mocker: MockerFixture): 26 | fail_python_version(mocker, "3.8.0") 27 | 28 | 29 | def test_fail_python_2_7(mocker: MockerFixture): 30 | fail_python_version(mocker, "2.7.0") 31 | 32 | 33 | def test_fail_cookiecutter_1_6_0(mocker: MockerFixture): 34 | fail_cookiecutter_version(mocker, "1.6.0") 35 | 36 | 37 | def test_fail_cookiecutter_1_7_1(mocker: MockerFixture): 38 | fail_cookiecutter_version(mocker, "1.7.1") 39 | 40 | 41 | def fail_python_version(mocker: MockerFixture, version_info: str): 42 | patch_cookiecutter_run_script( 43 | mocker, lambda mocker: mocker.patch('platform.python_version', lambda: version_info)) 44 | with pytest.raises(SystemExit): 45 | check_project() 46 | 47 | 48 | def fail_cookiecutter_version(mocker: MockerFixture, version_info: str): 49 | patch_cookiecutter_run_script( 50 | mocker, lambda mocker: mocker.patch('cookiecutter.__version__', version_info)) 51 | with pytest.raises(SystemExit): 52 | check_project() 53 | 54 | 55 | def patch_cookiecutter_run_script(mocker: MockerFixture, patch_fun: Callable[[MockerFixture], Any]): 56 | def run_script_wrapper(script_path, *args, **kwargs): 57 | patch_fun(mocker) 58 | run_path(script_path) 59 | mocker.patch('cookiecutter.hooks.run_script', run_script_wrapper) 60 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | from tempfile import TemporaryDirectory 6 | from typing import Callable, Dict, List, Union 7 | 8 | from cookiecutter.main import cookiecutter 9 | 10 | root = str(Path(__file__).parent.parent) 11 | 12 | expected_files_base = [ 13 | '.editorconfig', 14 | '.gitignore', 15 | '.pre-commit-config.yaml', 16 | 'README.md', 17 | 'src/{module_name}/__init__.py', 18 | 'src/{module_name}/main.py', 19 | 'src/{module_name}/version.py', 20 | ] 21 | 22 | 23 | def get_module_name(project_dir: Path) -> str: 24 | module_name = os.listdir(project_dir / 'src')[0] 25 | assert (project_dir / 'src' / module_name).is_dir() 26 | return module_name 27 | 28 | 29 | def resolve_module_dir(files: List[str], module_name: str) -> List[str]: 30 | return [(s.format(module_name=module_name) if '{' in s else s) for s in files] if files else [] 31 | 32 | 33 | def check_files(project_dir: Path, files: List[str], exist=True): 34 | for file in files: 35 | path = (project_dir / file).resolve() 36 | assert path.exists() == exist, f"file '{path}' should {'' if exist else 'not '}have existed" 37 | 38 | 39 | def list_files(base_dir, indent=4): 40 | for base, dirs, files in os.walk(base_dir): 41 | level = len(Path(base).relative_to(base_dir).parents) 42 | space = ' ' * indent * level 43 | print(f'{space}{os.path.basename(base)}/') 44 | space_sub = ' ' * indent * (level + 1) 45 | for f in files: 46 | print(f'{space_sub}{f}') 47 | 48 | 49 | def check_project( 50 | project_name="Test Project", settings: Dict[str, str] = None, files_existent: List = None, 51 | files_non_existent: List = None, test_cli=False, run_pytest=False, 52 | fun: Callable[[Path], None] = None): 53 | # define cookiecutter settings 54 | if settings is None: 55 | settings = {'project_name': project_name} 56 | else: 57 | settings['project_name'] = project_name 58 | # work in a new temporary directory 59 | with TemporaryDirectory() as temp_dir: 60 | # create the project files from the cookiecutter template 61 | project_dir = cookiecutter(root, extra_context=settings, no_input=True, output_dir=temp_dir) 62 | project_dir = Path(project_dir) 63 | module_name = get_module_name(project_dir) 64 | src_dir = str(project_dir / 'src') 65 | list_files(project_dir) 66 | # check that certain files exist and make sure that others do not exist 67 | paths_pos = resolve_module_dir(expected_files_base + (files_existent or []), module_name) 68 | paths_neg = resolve_module_dir(files_non_existent, module_name) 69 | check_files(project_dir, paths_pos) 70 | check_files(project_dir, paths_neg, exist=False) 71 | # test the CLI 72 | if test_cli: 73 | result = subprocess.run([sys.executable, '-m', module_name, '--version'], cwd=src_dir) 74 | assert result.returncode == 0, "cli call returned a nonzero exit code" 75 | # run pytests, if specified 76 | if run_pytest: 77 | result = subprocess.run([sys.executable, '-m', 'pytest', '..'], cwd=src_dir) 78 | assert result.returncode == 0, "some pytest cases had errors" 79 | # run additional code, if specified 80 | if fun: 81 | fun(project_dir) 82 | 83 | 84 | def assert_file_contains(file: Union[str, Path], contains: str = None, not_contains: str = None): 85 | with open(file, 'r') as fp: 86 | content = fp.read() 87 | if contains: 88 | assert contains in content, f"'{contains}' should have been found in '{file}'" 89 | if not_contains: 90 | assert not_contains not in content, f"'{contains}' should not exist in '{file}'" 91 | -------------------------------------------------------------------------------- /tests/version_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import shutil 4 | import subprocess 5 | import sys 6 | import tempfile 7 | 8 | expect_fail = '--fail' in sys.argv 9 | actual_fail = False 10 | 11 | # run cookiecutter in a subprocess (so we can catch terminal output) 12 | root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 13 | temp_dir = tempfile.mkdtemp() 14 | return_code = 1 15 | try: 16 | p = subprocess.Popen( 17 | [sys.executable, '-m', 'cookiecutter', '--no-input', '-o', f'"{temp_dir}"', '.'], 18 | cwd=root, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 19 | stdout, stderr = p.communicate() 20 | return_code = p.returncode 21 | actual_fail = return_code != 0 22 | finally: 23 | shutil.rmtree(temp_dir, ignore_errors=True) 24 | 25 | # handle possible issues & give proper return codes 26 | if b'Python 3.9 or higher' in stdout or b'successfully created' in stdout: 27 | status = {True: "failed", False: "succeded"} 28 | if actual_fail == expect_fail: 29 | print(f"Python {platform.python_version()} {status[expect_fail]} as expected") 30 | else: 31 | print( 32 | f"Python {platform.python_version()} should have {status[expect_fail]},", 33 | f"but actually {status[actual_fail]}", 34 | ) 35 | sys.exit(return_code) 36 | elif b'SyntaxError' in stderr: 37 | print(f"got a syntax error in pre_gen_project.py:\n{stderr}") 38 | sys.exit(return_code) 39 | else: 40 | print(f"unexpected error, code {return_code}") 41 | print(f"stderr: {stderr}") 42 | print(f"stdout: {stdout}") 43 | sys.exit(return_code) 44 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.dockerignore: -------------------------------------------------------------------------------- 1 | # IDE / Editor config 2 | .git 3 | .idea/ 4 | .vscode/ 5 | 6 | # Python build artifacts 7 | build/ 8 | dist/ 9 | *.egg-info/ 10 | .eggs/ 11 | pip-wheel-metadata/ 12 | 13 | # Virtual environments 14 | .venv/ 15 | venv*/ 16 | env*/ 17 | 18 | # Python cache 19 | __pycache__/ 20 | *.py[cod] 21 | *.so 22 | .pytest_cache/ 23 | .cache/ 24 | htmlcov/ 25 | 26 | # Docker-specific 27 | Dockerfile 28 | docker-compose.yml 29 | 30 | # Jupyter 31 | .ipynb_checkpoints/ 32 | 33 | # Logs and misc 34 | *.log 35 | *.tmp 36 | *.bak 37 | *.swp 38 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | charset = utf-8 10 | end_of_line = lf 11 | 12 | [*.py] 13 | indent_size = 4 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.{conf,js,json,yml,yaml,xml}] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # Unit test / coverage reports 29 | htmlcov/ 30 | junit/ 31 | .tox/ 32 | .coverage 33 | .coverage.* 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | *.cover 38 | .hypothesis/ 39 | .pytest_cache/ 40 | 41 | # Django stuff: 42 | *.log 43 | local_settings.py 44 | 45 | # Flask stuff: 46 | instance/ 47 | .webassets-cache 48 | 49 | # Sphinx documentation 50 | docs/_build/ 51 | 52 | # PyBuilder 53 | target/ 54 | 55 | # Jupyter Notebook 56 | .ipynb_checkpoints 57 | 58 | # pyenv 59 | .python-version 60 | 61 | # virtualenv 62 | .env 63 | .venv 64 | venv*/ 65 | 66 | # OS specific 67 | .DS_Store 68 | 69 | # IDE settings 70 | .vscode/ 71 | .idea/ 72 | *.iws 73 | 74 | # project-specific stuff 75 | # exclude data from source control (but keep the folder) 76 | data/* 77 | !data/.gitkeep 78 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # templates 2 | # ------------ 3 | 4 | .deploy-template: 5 | image: alpine:latest 6 | script: 7 | - echo "Deploying to ${CI_ENVIRONMENT_NAME}" 8 | variables: 9 | GIT_STRATEGY: none # prevents fetching repo (we only need the artifact here) 10 | 11 | # ------------- 12 | # Pipeline Definition 13 | 14 | variables: 15 | PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip 16 | {%- if cookiecutter.package_manager == 'conda' %} 17 | CONDA_PKGS_DIRS: $CI_PROJECT_DIR/.cache/conda 18 | {%- elif cookiecutter.package_manager == 'poetry' %} 19 | POETRY_VERSION: 2.1.1 20 | POETRY_CACHE_DIR: $CI_PROJECT_DIR/.cache/poetry 21 | {%- endif %} 22 | 23 | stages: 24 | - build 25 | - test 26 | - deploy-nonprod 27 | - deploy-prod 28 | 29 | # Build and Test 30 | 31 | {% if cookiecutter.package_manager == 'poetry' -%} 32 | build-wheel: 33 | image: python:3.9 34 | stage: build 35 | artifacts: 36 | name: 'app-wheel' 37 | paths: 38 | - dist/{{ cookiecutter.module_name }}-*.whl 39 | expire_in: 6 mos 40 | script: 41 | - pip install poetry==$POETRY_VERSION 42 | - poetry build -f wheel{% else -%} 43 | build-wheel: 44 | image: python:3.9 45 | stage: build 46 | artifacts: 47 | name: 'app-wheel' 48 | paths: 49 | - dist/{{ cookiecutter.module_name }}-*.whl 50 | expire_in: 6 mos 51 | script: 52 | - python setup.py bdist_wheel{%- endif %} 53 | 54 | {% if cookiecutter.package_manager == 'poetry' -%} 55 | test-unit: 56 | stage: test 57 | image: python:3.9 58 | cache: 59 | key: 60 | files: 61 | - poetry.lock 62 | paths: 63 | - $PIP_CACHE_DIR 64 | - $POETRY_CACHE_DIR 65 | artifacts: 66 | reports: 67 | coverage_report: 68 | coverage_format: cobertura 69 | path: coverage.xml 70 | coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' 71 | before_script: 72 | - pip install poetry==$POETRY_VERSION 73 | - poetry install --only=main,test 74 | - source `poetry env info --path`/bin/activate 75 | script: 76 | - pytest tests --cov src --cov-report=term --cov-report xml:coverage.xml{% elif cookiecutter.package_manager == 'conda' -%} 77 | test-unit: 78 | stage: test 79 | image: continuumio/miniconda3 80 | cache: 81 | key: 82 | files: 83 | - environment.yml 84 | - environment-dev.yml 85 | paths: 86 | - $PIP_CACHE_DIR 87 | - $CONDA_PKGS_DIRS/*.conda 88 | - $CONDA_PKGS_DIRS/*.tar.bz2 89 | - $CONDA_PKGS_DIRS/urls* 90 | - $CONDA_PKGS_DIRS/cache 91 | before_script: 92 | - export PATH="/opt/conda/bin:$PATH" 93 | - conda env create -n .venv -f environment-dev.yml environment.yml 94 | - source activate .venv 95 | - pip install dist/{{ cookiecutter.module_name }}-*.whl 96 | script: 97 | - pytest tests{% elif cookiecutter.package_manager == 'pip' -%} 98 | test-unit: 99 | stage: test 100 | image: python:3.9 101 | cache: 102 | key: 103 | files: 104 | - requirements.txt 105 | - requirements-dev.txt 106 | paths: 107 | - $PIP_CACHE_DIR 108 | before_script: 109 | - python -m venv .venv 110 | - source .venv/bin/activate 111 | - pip install -r requirements.txt -r requirements-dev.txt 112 | - pip install dist/{{ cookiecutter.module_name }}-*.whl 113 | script: 114 | - pytest tests{%- endif %} 115 | 116 | # Nonprod deployments 117 | 118 | dev: 119 | extends: .deploy-template 120 | stage: deploy-nonprod 121 | environment: 122 | name: dev 123 | only: 124 | - branches 125 | 126 | # Prod deployment 127 | 128 | prod: 129 | extends: .deploy-template 130 | stage: deploy-prod 131 | environment: 132 | name: prod 133 | only: 134 | - master 135 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.idea__editor/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.idea__editor/{{cookiecutter.project_slug}}.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: 'v5.0.0' 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-ast 7 | - id: check-builtin-literals 8 | - id: check-merge-conflict 9 | - id: check-toml 10 | - id: check-yaml 11 | - id: detect-private-key 12 | - id: end-of-file-fixer 13 | - id: mixed-line-ending 14 | - id: trailing-whitespace{% if cookiecutter.code_formatter == 'black' %} 15 | - repo: https://github.com/psf/black 16 | rev: 'stable' 17 | hooks: 18 | - id: black 19 | language_version: python3.9 20 | exclude: ^notebooks{% else %} 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: 'v0.11.12' 23 | hooks: 24 | - id: ruff{% endif %} 25 | - repo: https://github.com/pycqa/isort 26 | rev: '6.0.1' 27 | hooks: 28 | - id: isort 29 | - repo: https://github.com/asottile/pyupgrade 30 | rev: 'v3.20.0' 31 | hooks: 32 | - id: pyupgrade 33 | args: [ --py39-plus ] 34 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.vscode__editor/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Avoid vscode getting in trouble by watching too many files at once that we don't want to edit anyway 3 | "files.watcherExclude": { 4 | "**/.git/objects/**": true, 5 | "**/.git/subtree-cache/**": true, 6 | "**/.venv/**": true 7 | }, 8 | // Use pytest as our test framework 9 | "python.testing.pytestEnabled": true, 10 | // Automatically organize imports 11 | "editor.codeActionsOnSave": {"source.organizeImports": true}, 12 | {% if cookiecutter.code_formatter == 'black' %} // Use black as our formatter and run it every time we save a file 13 | "python.formatting.provider": "black", 14 | // TODO: Insert path to black installation, e.g.: 15 | // "python.formatting.blackPath": ".venv/bin/black", 16 | "editor.formatOnSave": true, 17 | "editor.defaultFormatter": "ms-python.python" 18 | {% endif -%} 19 | // Other useful (categories of) settings to look at are 20 | // "python.linting" 21 | // "autoDocstring.docstringFormat" (requires the "Python Docstring Generator" extension) 22 | } 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Dockerfile__conda: -------------------------------------------------------------------------------- 1 | ARG IMAGE_TAG=24.7.1-2 2 | 3 | FROM condaforge/mambaforge:${IMAGE_TAG} 4 | 5 | LABEL maintainer="{{ cookiecutter.company_name if cookiecutter.company_name else cookiecutter.full_name }}" 6 | 7 | WORKDIR /{{cookiecutter.module_name}} 8 | COPY environment.yml . 9 | 10 | RUN conda config --set channel_priority strict && \ 11 | mamba env create -n {{cookiecutter.module_name}}_env -f environment.yml 12 | 13 | # Make RUN commands use the new environment (see: https://pythonspeed.com/articles/activate-conda-dockerfile/) 14 | SHELL ["mamba", "run", "-n", "{{cookiecutter.module_name}}_env", "/bin/bash", "-c"] 15 | 16 | COPY . . 17 | RUN python setup.py install 18 | 19 | # ENTRYPOINT doesn't use the same shell as RUN so you need the conda stuff 20 | ENTRYPOINT ["mamba", "run", "-n", "{{cookiecutter.module_name}}_env", "python", "-OO", "-m", "{{ cookiecutter.module_name }}"] 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Dockerfile__pip: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.13 2 | FROM python:${PYTHON_VERSION}-bullseye 3 | LABEL maintainer="{{ cookiecutter.company_name if cookiecutter.company_name else cookiecutter.full_name }}" 4 | 5 | WORKDIR /app 6 | COPY requirements.txt setup.py ./ 7 | RUN pip install -r requirements.txt --no-cache-dir --prefer-binary 8 | COPY ./src ./src 9 | COPY README.md ./ 10 | RUN pip install . 11 | 12 | ENTRYPOINT ["python", "-OO", "-m", "{{ cookiecutter.module_name }}"] 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Dockerfile__poetry: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.13 2 | FROM python:${PYTHON_VERSION}-bullseye 3 | LABEL maintainer="{{ cookiecutter.company_name if cookiecutter.company_name else cookiecutter.full_name }}" 4 | 5 | RUN pip install poetry && poetry config virtualenvs.create false 6 | WORKDIR /app 7 | COPY pyproject.toml poetry.lock ./ 8 | RUN poetry install --only main --no-root --no-interaction && \ 9 | poetry cache clear pypi --all 10 | COPY ./src /app/src 11 | RUN poetry install --only-root 12 | 13 | ENTRYPOINT ["python", "-OO", "-m", "{{ cookiecutter.module_name }}"] 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Dockerfile__uv: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:python3.11-bookworm AS builder 2 | 3 | LABEL maintainer="{{ cookiecutter.company_name if cookiecutter.company_name else cookiecutter.full_name }}" 4 | 5 | WORKDIR /app 6 | 7 | COPY pyproject.toml uv.lock* ./ 8 | 9 | RUN uv sync --frozen --no-install-project && uv cache clean 10 | 11 | COPY ./src /app/src 12 | 13 | RUN uv sync --frozen && uv cache clean 14 | 15 | FROM python:3.13-slim-bookworm 16 | 17 | WORKDIR /app 18 | 19 | COPY --from=builder /app/.venv /app/.venv 20 | COPY --from=builder /app/src /app/src 21 | 22 | COPY ./config /app/config 23 | 24 | # Environment variables 25 | ENV PATH="/app/.venv/bin:$PATH" \ 26 | PYTHONUNBUFFERED=1 \ 27 | PYTHONDONTWRITEBYTECODE=1 28 | 29 | # Run the application 30 | ENTRYPOINT ["python", "-OO", "-m", "{{ cookiecutter.module_name }}"] 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/README.md: -------------------------------------------------------------------------------- 1 | {#- ------------------------------------------ -#} 2 | {#- Definition of Template Variables -#} 3 | {#- ------------------------------------------ -#} 4 | {%- set py_command = 'poetry run python' if cookiecutter.package_manager == 'poetry' else 'python' -%} 5 | {%- set test_command = 'poetry run pytest tests' if cookiecutter.package_manager == 'poetry' else 'python setup.py test' -%} 6 | {%- set test_cov_command = 'poetry run pytest tests --cov=src --cov-report=xml' if cookiecutter.package_manager == 'poetry' else 'python setup.py testcov' -%} 7 | {%- set build_command = 'poetry build' if cookiecutter.package_manager == 'poetry' else 'python setup.py dist' -%} 8 | {%- set install_command = 'poetry add' if cookiecutter.package_manager == 'poetry' else 'conda install' if cookiecutter.package_manager == 'conda' else 'pip install' -%} 9 | {#- ------------------------------------------ -#} 10 | # {{ cookiecutter.project_name }} 11 | 12 | {{ cookiecutter.project_short_description }} 13 | 14 | ## Getting Started 15 | {% if cookiecutter.package_manager == 'conda' %} 16 | To set up your local development environment, please use a fresh virtual environment with: 17 | 18 | conda env create --name {{ cookiecutter.project_slug }} --file=environment-dev.yml 19 | 20 | Then activate the environment with: 21 | 22 | conda activate {{ cookiecutter.project_slug }} 23 | 24 | To update this environment with your production dependencies, please run: 25 | 26 | conda env update --file=environment.yml 27 | {% elif cookiecutter.package_manager == 'pip' %} 28 | To set up your local development environment, please use a fresh virtual environment (`python -m venv .venv`), then run: 29 | 30 | pip install -r requirements.txt -r requirements-dev.txt 31 | pip install -e . 32 | 33 | The first command will install all requirements for the application and to execute tests. 34 | With the second command, you'll get an editable installation of the module, so that imports work properly. 35 | {% elif cookiecutter.package_manager == 'poetry' %} 36 | To set up your local development environment, please run: 37 | 38 | poetry install 39 | 40 | Behind the scenes, this creates a virtual environment and installs `{{ cookiecutter.module_name }}` along with its dependencies into a new virtualenv. 41 | Whenever you run `poetry run `, that `` is actually run inside the virtualenv managed by poetry. 42 | 43 | {% elif cookiecutter.package_manager == 'uv' %} 44 | ## Using `uv` for Project Setup 45 | 46 | > 🧰 **Note:** You do **not** need to have Python pre-installed. 47 | > `uv` includes its own Python runtime and manages everything automatically. 48 | 49 | ### 🔧 Installing `uv` 50 | 51 | Install `uv` using the official script: 52 | 53 | > For macOS and Linux (bash): 54 | ```bash 55 | curl -LsSf https://astral.sh/uv/install.sh | sh 56 | ``` 57 | 58 | > For Windows (PowerShell): 59 | 60 | ```powershell 61 | powershell -c "irm https://astral.sh/uv/install.ps1 | more" 62 | ``` 63 | 64 | For more install options, see: [https://docs.astral.sh/uv/getting-started/installation/](https://docs.astral.sh/uv/getting-started/installation/) 65 | 66 | --- 67 | 68 | ### 🚀 Getting Started 69 | 70 | 1. **Install all dependencies defined in `pyproject.toml`:** 71 | 72 | ```bash 73 | uv sync 74 | ``` 75 | 76 | 2. **(Optional) Add new dependencies during development:** 77 | 78 | ```bash 79 | uv add 80 | ``` 81 | 82 | Example: 83 | 84 | ```bash 85 | uv add requests 86 | ``` 87 | 88 | 3. **Run Python or project scripts:** 89 | 90 | Recommended: 91 | 92 | ```bash 93 | uv run python 94 | ``` 95 | 96 | Or, if using scripts defined in `pyproject.toml`: 97 | 98 | ```bash 99 | uv run 100 | ``` 101 | 102 | --- 103 | 104 | ### ⚙️ Alternative: Manual virtual environment activation 105 | 106 | If you prefer using `python` directly, activate the `uv`-managed virtual environment: 107 | 108 | - macOS/Linux: 109 | 110 | ```bash 111 | source .venv/bin/activate 112 | ``` 113 | 114 | - Windows (CMD): 115 | 116 | ```cmd 117 | .venv\Scripts\activate 118 | ``` 119 | 120 | - Windows (PowerShell): 121 | 122 | ```powershell 123 | .venv\Scripts\Activate.ps1 124 | ``` 125 | 126 | Then run: 127 | 128 | ```bash 129 | python your_script.py 130 | ``` 131 | 132 | To deactivate: 133 | 134 | ```bash 135 | deactivate 136 | ``` 137 | {% endif -%} 138 | 139 | {% if cookiecutter.create_cli == 'yes' %} 140 | You can now access the CLI with `{{ py_command }} -m {{ cookiecutter.module_name }}`. 141 | {% else %} 142 | You can now import functions and classes from the module with `import {{ cookiecutter.module_name }}`. 143 | {% endif -%} 144 | 145 | {% if cookiecutter.use_docker == 'yes' %} 146 | If you want to deploy this project as a docker container, please ensure that [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed, then run 147 | 148 | docker-compose up 149 | 150 | this will build the entire project with all dependencies inside a docker container. You may use the command line interface of the application now, e.g. by editing the `command` tag in the [`docker-compose.yml`](./docker-compose.yml). 151 | {% endif %} 152 | ### Testing 153 | 154 | We use `pytest` as the test framework. To run tests, use: 155 | 156 | {% if cookiecutter.package_manager == 'poetry' %} 157 | poetry run pytest tests 158 | {% elif cookiecutter.package_manager == 'uv' %} 159 | uv run pytest tests 160 | {% else %} 161 | pytest tests 162 | {% endif %} 163 | 164 | To run tests with coverage reporting: 165 | 166 | {% if cookiecutter.package_manager == 'poetry' %} 167 | poetry run pytest tests --cov=src --cov-report=html --cov-report=term 168 | {% elif cookiecutter.package_manager == 'uv' %} 169 | uv run pytest tests --cov=src --cov-report=html --cov-report=term 170 | {% else %} 171 | pytest tests --cov=src --cov-report=html --cov-report=term 172 | {% endif %} 173 | 174 | After running the tests, open the `htmlcov` directory in your browser to inspect coverage visually. 175 | 176 | {% if cookiecutter.use_notebooks == 'yes' %} 177 | ### Notebooks 178 | 179 | You can use your module code (`src/`) in Jupyter notebooks (`notebooks/`) without running into import errors. 180 | 181 | {% if cookiecutter.package_manager == 'poetry' %} 182 | Launch the Jupyter server from the project's virtualenv: 183 | 184 | poetry run jupyter notebook 185 | 186 | or 187 | 188 | poetry run jupyter-lab 189 | 190 | {% elif cookiecutter.package_manager == 'uv' %} 191 | Launch Jupyter notebooks within the `uv` environment: 192 | 193 | uv run jupyter notebook 194 | 195 | or 196 | 197 | uv run jupyter-lab 198 | 199 | {% elif cookiecutter.package_manager == 'conda' %} 200 | Make sure your conda environment is activated, then launch: 201 | 202 | jupyter notebook 203 | 204 | or 205 | 206 | jupyter lab 207 | 208 | {% else %} 209 | First, make sure to install your package in editable mode: 210 | 211 | pip install -e . 212 | 213 | Then launch Jupyter: 214 | 215 | jupyter notebook 216 | 217 | or 218 | 219 | jupyter lab 220 | {% endif %} 221 | 222 | To make your virtual environment available as a Jupyter kernel, run: 223 | 224 | {{ install_command }} ipykernel 225 | {{ py_command }} -m ipykernel install --user --name="{{ cookiecutter.project_slug }}" 226 | 227 | > 💡 This ensures that your notebook environment uses the same dependencies and paths as your project. 228 | 229 | Note: We mainly use notebooks for experimentation, visualizations, and reporting. Any reusable logic should live in the `src/` module and be imported into notebooks. 230 | {% endif %} 231 | ### Distribution Package 232 | 233 | To build a distribution package (wheel), run: 234 | 235 | {% if cookiecutter.package_manager == 'poetry' %} 236 | poetry build 237 | {% elif cookiecutter.package_manager == 'uv' %} 238 | uv run python -m build 239 | {% elif cookiecutter.package_manager == 'conda' %} 240 | python setup.py sdist bdist_wheel 241 | {% else %} 242 | python setup.py bdist_wheel 243 | {% endif %} 244 | 245 | {% if cookiecutter.package_manager == 'uv' %} 246 | > 💡 If `build` is not yet added to your project, install it with: 247 | 248 | uv add --dev build 249 | {% elif cookiecutter.package_manager == 'poetry' %} 250 | > 💡 `poetry build` handles everything including packaging, metadata, and versioning. 251 | {% elif cookiecutter.package_manager == 'pip' %} 252 | > 💡 Make sure `wheel` and `build` are installed: 253 | 254 | pip install build wheel 255 | {% elif cookiecutter.package_manager == 'conda' %} 256 | > 💡 If needed, install wheel via: 257 | 258 | conda install wheel 259 | {% endif %} 260 | 261 | Build artifacts will be placed in the `dist/` directory. 262 | 263 | 264 | ### Contributions 265 | 266 | Before contributing, please set up the pre-commit hooks to ensure consistent formatting and linting. 267 | 268 | {% if cookiecutter.package_manager == 'poetry' %} 269 | Install the hooks with: 270 | 271 | poetry run pre-commit install 272 | {% elif cookiecutter.package_manager == 'uv' %} 273 | Install the hooks with: 274 | 275 | uv run pre-commit install 276 | {% else %} 277 | pip install -U pre-commit 278 | pre-commit install 279 | {% endif %} 280 | 281 | > This will automatically run checks like code formatting, import sorting, and linting before each commit. 282 | 283 | To uninstall the hooks: 284 | 285 | {% if cookiecutter.package_manager == 'poetry' %} 286 | poetry run pre-commit uninstall 287 | {% elif cookiecutter.package_manager == 'uv' %} 288 | uv run pre-commit uninstall 289 | {% else %} 290 | pre-commit uninstall 291 | {% endif %} 292 | 293 | ## Contact 294 | 295 | {{ cookiecutter.full_name }} ({{ cookiecutter.email }}) 296 | 297 | ## License 298 | 299 | {% if cookiecutter.company_name %}© {{cookiecutter.company_name}}{% else %}© {{cookiecutter.full_name}}{% endif %} 300 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/config.yml: -------------------------------------------------------------------------------- 1 | # log configuration for {{cookiecutter.module_name}} 2 | logging: 3 | enabled: true 4 | format: "%(asctime)s.%(msecs)03d [%(levelname)s]: %(message)s" 5 | level: "INFO" 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/dev.conf: -------------------------------------------------------------------------------- 1 | environment = "dev" 2 | logging.level = DEBUG # overrides the log level that is specified in res/default.conf 3 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/config/prod.conf: -------------------------------------------------------------------------------- 1 | environment = "prod" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/at-gmbh/at-python-template/f167921eca9cf2bbac387a5890a2f93ca5304826/{{cookiecutter.project_slug}}/data/.gitkeep -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | {{ cookiecutter.project_slug }}: 5 | image: 'at/{{ cookiecutter.project_slug }}:latest' 6 | build: 7 | context: . 8 | args: 9 | {% if cookiecutter.package_manager == 'uv' %} 10 | PYTHON_IMAGE_TAG: "3.11-bookworm" 11 | {% elif cookiecutter.package_manager == 'poetry' %} 12 | PYTHON_IMAGE_TAG: "3.9-bullseye" 13 | {% elif cookiecutter.package_manager == 'conda' %} 14 | PYTHON_IMAGE_TAG: "3.9-slim" 15 | {% else %} 16 | PYTHON_IMAGE_TAG: "3.9-slim" 17 | {% endif %} 18 | command: '{% if cookiecutter.create_cli == "yes" %}--help{% else %}--config /app/config/config.yml{% endif %}' 19 | tty: true 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/environment-dev.yml: -------------------------------------------------------------------------------- 1 | # This file defines the additional requirements for a developer working 2 | # with this project (e.g. for testing or useful development tools). 3 | # The regular project dependencies are defined in environment.yml 4 | 5 | channels: 6 | - conda-forge 7 | - defaults 8 | 9 | dependencies: 10 | - python>=3.9 11 | - pytest>=8.3.4 12 | - pytest-cov>=6.0.0 13 | - pre-commit>=4.1.0 14 | - isort>=6.0.0{% if cookiecutter.use_notebooks == 'yes' %} 15 | - jupyterlab>=4.3.5{% endif %} 16 | - pip{% if cookiecutter.code_formatter == 'black' %} 17 | - pip: 18 | - black>=25.1.0{% else %} 19 | - pip: 20 | - ruff>=0.11.0{% endif %} 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/environment.yml: -------------------------------------------------------------------------------- 1 | # This file defines the requirements for a productive project deployment. 2 | # It should not contain dependencies for tests, developer tools, etc. 3 | # For development-related dependencies, refer to environment-dev.yml. 4 | 5 | channels: 6 | - conda-forge 7 | - defaults 8 | 9 | dependencies: 10 | - python>=3.9{% if cookiecutter.config_file == 'hocon' %} 11 | - pyhocon>=0.3.61{% elif cookiecutter.config_file == 'yaml' %} 12 | - PyYAML>=6.0.2{% endif %} 13 | - pip {% if cookiecutter.create_cli == 'yes' %} 14 | - pip: 15 | - typer==0.15.1{% endif %} 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/notebooks/example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | {% if cookiecutter.package_manager == 'poetry' %} 8 | "You can use your module `{{ cookiecutter.module_name }}` from here, provided you're running the jupyter server using `poetry run jupyter notebook` from the root of this project directory." 9 | {% else %} 10 | "To use your module `{{ cookiecutter.module_name }}` from here, run `pip install -e .` from the root of your project directory." 11 | {% endif %} 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": ["%load_ext autoreload\n%autoreload 2"] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [] 27 | } 28 | ], 29 | "metadata": { 30 | "language_info": { 31 | "file_extension": ".py", 32 | "name": "python" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 4 37 | } 38 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.package_manager == "poetry" -%} 2 | [tool.poetry] 3 | name = "{{ cookiecutter.project_slug }}" 4 | version = "0.1.0" 5 | description = "{{ cookiecutter.project_short_description }}" 6 | authors = ["{{ cookiecutter.full_name }} <{{ cookiecutter.email }}>"] 7 | license = "proprietary" 8 | packages = [{ include = "{{ cookiecutter.module_name }}", from = "src" }] 9 | include = ["src/{{ cookiecutter.module_name }}/res/*"] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.9" 13 | {%- if cookiecutter.config_file == 'hocon' %} 14 | pyhocon = "^0.3.59" 15 | {%- elif cookiecutter.config_file == 'yaml' %} 16 | PyYAML = "^6.0" 17 | {%- endif %} 18 | {%- if cookiecutter.create_cli == 'yes' %} 19 | typer = "^0.16.0" 20 | {%- endif %} 21 | 22 | [tool.poetry.group.test.dependencies] 23 | pytest = "^8.4.0" 24 | pytest-cov = "^4.0" 25 | 26 | [tool.poetry.group.linter.dependencies] 27 | {%- if cookiecutter.code_formatter == 'black' %} 28 | black = "^25.1.0" 29 | {%- else %} 30 | ruff = "^0.1.7" 31 | {%- endif %} 32 | isort = "^5.12.0" 33 | 34 | [tool.poetry.group.dev.dependencies] 35 | pre-commit = "^3.0" 36 | {%- if cookiecutter.use_notebooks == 'yes' %} 37 | jupyterlab = "^3.5" 38 | {%- endif %} 39 | 40 | [tool.poetry.extras] 41 | build = ["setuptools", "cython"] 42 | 43 | [tool.poetry.scripts] 44 | {%- if cookiecutter.create_cli == "yes" %} 45 | {{ cookiecutter.project_slug }} = "{{ cookiecutter.module_name }}.main:app" 46 | {%- endif %} 47 | 48 | [tool.isort] 49 | {%- if cookiecutter.code_formatter == 'black' %} 50 | profile = "black" 51 | {%- else %} 52 | line_length = 100 53 | multi_line_output = 3 54 | {%- endif %} 55 | 56 | [tool.pytest.ini_options] 57 | minversion = "7.0" 58 | testpaths = ["tests"] 59 | 60 | [tool.uv] 61 | default-groups = ["dev", "test", "linter"] 62 | 63 | [build-system] 64 | requires = ["poetry-core"] 65 | build-backend = "poetry.core.masonry.api" 66 | 67 | {%- else %}[project] 68 | name = "{{ cookiecutter.project_slug }}" 69 | version = "0.1.0" 70 | description = "{{ cookiecutter.project_short_description }}" 71 | authors = [{ name = "{{ cookiecutter.full_name }}", email = "{{ cookiecutter.email }}" }] 72 | requires-python = ">=3.9" 73 | dependencies = [ 74 | {%- if cookiecutter.config_file == 'hocon' %} 75 | "pyhocon>=0.3.59", 76 | {%- elif cookiecutter.config_file == 'yaml' %} 77 | "PyYAML>=6.0", 78 | {%- endif %} 79 | {%- if cookiecutter.create_cli == 'yes' %} 80 | "typer>=0.16.0", 81 | {%- endif %} 82 | ] 83 | 84 | [project.license] 85 | text = "proprietary" 86 | 87 | [project.optional-dependencies] 88 | build = ["setuptools", "cython"] 89 | 90 | {%- if cookiecutter.create_cli == "yes" %} 91 | [project.scripts] 92 | {{ cookiecutter.project_slug }} = "{{ cookiecutter.module_name }}.main:app" 93 | {%- endif %} 94 | 95 | [project.gui-scripts] 96 | hello_gui = "{{ cookiecutter.module_name }}.gui:main" 97 | 98 | [project.entry-points."example.plugins"] 99 | a = "{{ cookiecutter.module_name }}_plugin_a" 100 | 101 | [dependency-groups] 102 | dev = ["pre-commit>=3.0"{% if cookiecutter.use_notebooks == 'yes' %}, "jupyterlab>=3.5"{% endif %}] 103 | linter = [ 104 | {%- if cookiecutter.code_formatter == 'black' %} 105 | "black>=25.1.0", 106 | {%- else %} 107 | "ruff>=0.1.7", 108 | {%- endif %} 109 | "isort>=5.12.0" 110 | ] 111 | test = ["pytest>=8.4.0", "pytest-cov>=4.0"] 112 | 113 | [tool.isort] 114 | {%- if cookiecutter.code_formatter == 'black' %} 115 | profile = "black" 116 | {%- else %} 117 | line_length = 100 118 | multi_line_output = 3 119 | {%- endif %} 120 | 121 | [tool.pytest.ini_options] 122 | minversion = "7.0" 123 | testpaths = ["tests"] 124 | 125 | [tool.uv] 126 | default-groups = ["dev", "test", "linter"] 127 | 128 | [build-system] 129 | requires = ["setuptools", "wheel"] 130 | build-backend = "setuptools.build_meta" 131 | {%- endif %} 132 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # This file defines the additional requirements for a developer working 2 | # with this project (e.g. for testing or useful development tools). 3 | # The regular project dependencies are defined in requirements.txt{% if cookiecutter.code_formatter == 'black' %} 4 | black~=25.1.0{% endif %} 5 | pre-commit~=4.1 6 | pytest~=8.4.0 7 | pytest-cov~=6.0 8 | wheel~=0.45.1 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file defines the requirements for a productive project deployment. 2 | # It should not contain dependencies for tests, developer tools, etc. 3 | # For development-related dependencies, please refer to requirements-dev.txt. 4 | 5 | # Dependency for configuration file format 6 | {% if cookiecutter.config_file == 'hocon' %} 7 | # HOCON configuration parser 8 | pyhocon~=0.3.61 9 | {% elif cookiecutter.config_file == 'yaml' %} 10 | # YAML configuration parser 11 | PyYAML~=6.0.2 12 | {% endif %} 13 | 14 | # Dependency for Command-Line Interface (CLI) if enabled 15 | {% if cookiecutter.create_cli == 'yes' %} 16 | # CLI framework (with optional dependencies for enhanced functionality) 17 | typer~=0.16.0 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from runpy import run_path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | # read the program version from version.py (without loading the module) 7 | __version__ = run_path('src/{{ cookiecutter.module_name }}/version.py')['__version__'] 8 | 9 | 10 | def read(fname): 11 | """Utility function to read the README file.""" 12 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 13 | 14 | 15 | setup( 16 | name="{{ cookiecutter.project_slug }}", 17 | version=__version__, 18 | author="{{ cookiecutter.full_name }}", 19 | author_email="{{ cookiecutter.email }}", 20 | description="{{ cookiecutter.project_short_description }}", 21 | license="proprietary", 22 | url="", 23 | packages=find_packages("src"), 24 | package_dir={"": "src"}, 25 | package_data={'{{ cookiecutter.module_name }}': ['res/*']}, 26 | long_description=read('README.md'), 27 | install_requires=[], 28 | tests_require=[ 29 | 'pytest', 30 | 'pytest-cov', 31 | 'pre-commit', 32 | ], 33 | platforms='any', 34 | python_requires='>=3.9', 35 | ) 36 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | {{ cookiecutter.project_name }} 3 | 4 | {{ cookiecutter.project_short_description }} 5 | """ 6 | __title__ = "{{ cookiecutter.project_slug }}" 7 | {% if cookiecutter.company_name -%} 8 | __copyright__ = "© {% now 'utc', '%Y' %} {{ cookiecutter.company_name }}" 9 | {% else -%} 10 | __copyright__ = "© {% now 'utc', '%Y' %} {{ cookiecutter.full_name }}" 11 | {% endif %} 12 | from .version import __version__ 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/__main__.py: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.create_cli == 'yes' %}from {{ cookiecutter.module_name }}.main import app 2 | 3 | app(){% else %}from {{ cookiecutter.module_name }}.main import main 4 | main(){% endif %} 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/main.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | # TODO your journey starts here 3 | print("hello :)") 4 | 5 | 6 | if __name__ == "__main__": 7 | main() 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/main__cli.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import typer 4 | 5 | from {{ cookiecutter.module_name }} import __title__, __version__{% if cookiecutter.config_file != 'none' %}, util{% endif %} 6 | 7 | logger = logging.getLogger('{{ cookiecutter.module_name }}') 8 | 9 | app = typer.Typer(name='{{ cookiecutter.module_name }}') 10 | 11 | 12 | def version_callback(version: bool): 13 | if version: 14 | typer.echo(f"{__title__} {__version__}") 15 | raise typer.Exit() 16 | 17 | 18 | ConfigOption = typer.Option( 19 | {% if cookiecutter.config_file == 'yaml' %}...{% else %}None{% endif %}, 20 | '-c', 21 | '--config', 22 | metavar='PATH', 23 | help="path to the program configuration" 24 | ) 25 | VersionOption = typer.Option( 26 | None, 27 | '-v', 28 | '--version', 29 | callback=version_callback, 30 | is_eager=True, 31 | help="print the program version and exit" 32 | ) 33 | 34 | 35 | @app.command() 36 | def main(config_file: str = ConfigOption, version: bool = VersionOption): 37 | """ 38 | {{ cookiecutter.project_short_description }} 39 | 40 | Note: This is the entry point of your command line application. The values of the CLI params 41 | that are passed to this application will show up als parameters to this function. 42 | This docstring is where you describe what your command line application does. 43 | Try running `python -m {{ cookiecutter.module_name }} --help` to see how this shows up in the 44 | command line. 45 | """ 46 | {% if cookiecutter.config_file != 'none' %}config = util.load_config(config_file) 47 | util.logging_setup(config){% endif %} 48 | logger.info("Looks like you're all set up. Let's get going!") 49 | # TODO your journey starts here 50 | 51 | 52 | if __name__ == "__main__": 53 | app() 54 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/res/default.conf: -------------------------------------------------------------------------------- 1 | 2 | # log configuration for {{cookiecutter.module_name}} 3 | logging { 4 | enabled = true 5 | format = "%(asctime)s.%(msecs)03d [%(levelname)s]: %(message)s" 6 | level = INFO 7 | } 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/util__hocon.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Union 4 | 5 | import importlib.resources as resources 6 | from pyhocon import ConfigTree, ConfigFactory 7 | 8 | logger = logging.getLogger('{{ cookiecutter.module_name }}') 9 | 10 | 11 | def get_resource_string(path: str, decode=True) -> Union[str, bytes]: 12 | """ 13 | Load a package resource (i.e. a file from within this package) 14 | 15 | :param path: the path, starting at the root of the current module (e.g. 'res/default.conf'). 16 | must be a string, not a Path object! 17 | :param decode: if true, decode the file contents as string (otherwise return bytes) 18 | :return: the contents of the resource file (as string or bytes) 19 | """ 20 | package = __name__.split('.')[0] # Get the top-level package name 21 | with resources.files(package).joinpath(path).open('rb') as f: 22 | s = f.read() 23 | return s.decode(errors='ignore') if decode else s 24 | 25 | 26 | def load_config(config_file: Union[str, Path] = None) -> ConfigTree: 27 | """ 28 | Load the config from the specified file and use it to override fields in the default config. 29 | If no config file is specified, only the default config is loaded. 30 | 31 | :param config_file: path of the config file to load 32 | :return: the parsed config 33 | """ 34 | base_config_str = get_resource_string('res/default.conf') 35 | config: ConfigTree = ConfigFactory.parse_string(base_config_str) 36 | if config_file: 37 | try: 38 | config = ConfigFactory.parse_file(config_file).with_fallback(config) 39 | except FileNotFoundError as e: 40 | logger.warning(f"The specified config could not be loaded (using defaults): {e}") 41 | return config 42 | 43 | 44 | def logging_setup(config: ConfigTree): 45 | """ 46 | setup logging based on the configuration 47 | 48 | :param config: the parsed config tree 49 | """ 50 | fmt = config.get('logging.format') 51 | if config.get_bool('logging.enabled'): 52 | level = logging._nameToLevel[config.get('logging.level').upper()] 53 | else: 54 | level = logging.NOTSET 55 | logging.basicConfig(format=fmt, level=logging.WARNING) 56 | logger.setLevel(level) 57 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/util__yaml.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Any, Dict, Union 4 | 5 | import importlib.resources as resources 6 | import yaml 7 | 8 | logger = logging.getLogger('{{ cookiecutter.module_name }}') 9 | 10 | 11 | def get_resource_string(path: str, decode=True) -> Union[str, bytes]: 12 | """ 13 | Load a package resource (i.e. a file from within this package) 14 | 15 | :param path: the path, starting at the root of the current module (e.g. 'res/default.conf'). 16 | must be a string, not a Path object! 17 | :param decode: if true, decode the file contents as string (otherwise return bytes) 18 | :return: the contents of the resource file (as string or bytes) 19 | """ 20 | package = __name__.split('.')[0] # Get the top-level package name 21 | with resources.files(package).joinpath(path).open('rb') as f: 22 | s = f.read() 23 | return s.decode(errors='ignore') if decode else s 24 | 25 | 26 | def load_config(config_file: Union[str, Path]) -> Dict[str, Any]: 27 | """ 28 | Load the config from the specified yaml file 29 | 30 | :param config_file: path of the config file to load 31 | :return: the parsed config as dictionary 32 | """ 33 | with open(config_file) as fp: 34 | return yaml.safe_load(fp) 35 | 36 | 37 | def logging_setup(config: Dict): 38 | """ 39 | setup logging based on the configuration 40 | 41 | :param config: the parsed config tree 42 | """ 43 | log_conf = config['logging'] 44 | fmt = log_conf['format'] 45 | if log_conf['enabled']: 46 | level = logging._nameToLevel[log_conf['level'].upper()] 47 | else: 48 | level = logging.NOTSET 49 | logging.basicConfig(format=fmt, level=logging.WARNING) 50 | logger.setLevel(level) 51 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/src/{{cookiecutter.module_name}}/version.py: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.package_manager == 'poetry' %}from importlib.metadata import version 2 | 3 | __version__ = version("{{ cookiecutter.project_slug }}") 4 | {%- else %}__version__ = '0.1.0'{% endif %} 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/at-gmbh/at-python-template/f167921eca9cf2bbac387a5890a2f93ca5304826/{{cookiecutter.project_slug}}/tests/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = 1 3 | log_cli_level = DEBUG 4 | log_cli_format = %(asctime)s.%(msecs)03d [%(levelname)s]: %(message)s (%(filename)s:%(lineno)s) 5 | log_cli_date_format=%Y-%m-%d %H:%M:%S 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/tests/test_example.py: -------------------------------------------------------------------------------- 1 | def test_the_universe(): 2 | assert 6 * 7 == 42 3 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/tests/test_util.py: -------------------------------------------------------------------------------- 1 | from {{ cookiecutter.module_name }} import util 2 | 3 | 4 | def test_get_resource_string(): 5 | s = util.get_resource_string('version.py') 6 | assert '__version__ = ' in s 7 | --------------------------------------------------------------------------------