├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── deploy_demo.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .somesy.toml ├── AUTHORS.md ├── CHANGELOG.md ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSES ├── CC0-1.0.txt └── MIT.txt ├── README.md ├── REUSE.toml ├── codemeta.json ├── fair_sw_mermaid.txt ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── src └── fair_python_cookiecutter │ ├── __init__.py │ ├── cli.py │ ├── config.py │ ├── main.py │ ├── template │ ├── cookiecutter.json │ ├── post_gen_project.bat │ └── {{ cookiecutter.project_slug }} │ │ ├── .github │ │ ├── ISSUE_TEMPLATE │ │ │ ├── bug_report.md │ │ │ └── feature_request.md │ │ └── workflows │ │ │ ├── ci.yml │ │ │ └── release.yml │ │ ├── .gitignore │ │ ├── .gitlab-ci.yml │ │ ├── .gitlab │ │ └── issue_templates │ │ │ └── Default.md │ │ ├── .pre-commit-config.yaml │ │ ├── AUTHORS.md │ │ ├── CHANGELOG.md │ │ ├── CITATION.cff │ │ ├── CODE_OF_CONDUCT.md │ │ ├── CONTRIBUTING.md │ │ ├── README.md │ │ ├── docs │ │ ├── changelog.md │ │ ├── code_of_conduct.md │ │ ├── contributing.md │ │ ├── credits.md │ │ ├── css │ │ │ └── style.css │ │ ├── dev_guide.md │ │ ├── index.md │ │ ├── license.md │ │ ├── logo.svg │ │ ├── overrides │ │ │ ├── main.html │ │ │ └── partials │ │ │ │ └── copyright.html │ │ ├── quickstart.md │ │ └── scripts │ │ │ ├── coverage_status.py │ │ │ └── gen_ref_pages.py │ │ ├── mkdocs.yml │ │ ├── pyproject.toml │ │ ├── somesy.toml │ │ ├── src │ │ └── {{ cookiecutter.project_package }} │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── cli.py │ │ │ └── lib.py │ │ ├── temp-REUSE.toml │ │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_api.py │ │ ├── test_cli.py │ │ └── test_lib.py │ └── utils.py └── tests ├── demo.yaml ├── test_main.py └── test_utils.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Traces** 26 | If applicable, provide a Python stack trace or JavaScript console log from the browser 27 | 28 | **Environment** 29 | Provide information about versions of relevant software packages. 30 | 31 | - Python Version (e.g. 3.8.10) 32 | - poetry version (e.g. 1.2.1) 33 | - Browser [e.g. chrome, safari] + Version 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | # Main CI pipeline of the repository. 3 | # 4 | # Overview: 5 | # Lint --> test doc build -\ 6 | # \-> test code ---> deploy docs (*) -> release (**) 7 | # 8 | # (*): only on push of primary branches + release tags 9 | # (**): only for release version tags (vX.Y.Z) 10 | 11 | on: 12 | push: 13 | branches: [main, dev] 14 | tags: ["v*.*.*"] 15 | pull_request: 16 | types: [opened, reopened, synchronize, ready_for_review] 17 | 18 | jobs: 19 | 20 | lint: 21 | # run general checks that do not require installing the package 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Install poetry 27 | run: pipx install poetry 28 | - uses: actions/setup-python@v5 29 | with: 30 | python-version: "3.10" 31 | 32 | - name: Install poe, pre-commit and safety 33 | run: pip install poethepoet pre-commit safety 34 | 35 | # NOTE: using custom cache, to include pre-commit linters + deps 36 | - uses: actions/cache@v4 37 | with: 38 | path: | 39 | ~/.cache/pre-commit 40 | ~/.cache/pip 41 | key: ${{ hashFiles('.pre-commit-config.yaml') }}-pre-commit 42 | 43 | - name: Check that all static analysis tools run without errors 44 | run: poetry run poe lint --all-files 45 | 46 | - name: Scan dependencies for known vulnerabilities 47 | run: safety check -r pyproject.toml 48 | 49 | test-build-docs: 50 | # make sure that documentation is buildable 51 | # (better to know that before e.g. a PR is merged) 52 | needs: lint 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Install poetry 58 | run: pipx install poetry 59 | - uses: actions/setup-python@v5 60 | with: 61 | python-version: "3.10" 62 | cache: "poetry" 63 | 64 | - name: Check that documentation builds without errors 65 | run: | 66 | poetry install --with docs 67 | poetry run poe docs 68 | 69 | test: 70 | # run tests with different OS and Python combinations 71 | needs: lint 72 | strategy: 73 | fail-fast: true 74 | matrix: 75 | os: [ "ubuntu-latest", "macos-latest", "windows-latest" ] 76 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] 77 | runs-on: ${{ matrix.os }} 78 | 79 | steps: 80 | - uses: actions/checkout@v4 81 | - name: Install poetry 82 | run: pipx install poetry 83 | - uses: actions/setup-python@v5 84 | with: 85 | python-version: ${{ matrix.python-version }} 86 | cache: "poetry" 87 | 88 | - name: Check that tests complete without errors 89 | run: | 90 | poetry install 91 | poetry run poe test 92 | 93 | docs: 94 | # build + deploy documentation (only on push event for certain branches+tags) 95 | needs: [test, test-build-docs] 96 | if: github.event_name == 'push' 97 | runs-on: ubuntu-latest 98 | permissions: 99 | contents: write 100 | 101 | steps: 102 | - uses: actions/checkout@v4 103 | - name: Install poetry 104 | run: pipx install poetry 105 | - uses: actions/setup-python@v5 106 | with: 107 | python-version: "3.10" 108 | cache: "poetry" 109 | 110 | - name: Install project with mkdocs and plugins 111 | run: poetry install --with docs 112 | 113 | - name: Configure Git user (Github Actions Bot) 114 | run: | 115 | git config --local user.name "github-actions[bot]" 116 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 117 | 118 | - name: Check out or initialize gh-pages branch 119 | run: | 120 | if git fetch origin gh-pages:gh-pages 121 | then 122 | echo "Found existing gh-pages branch." 123 | else 124 | echo "Creating new gh-pages branch and initializing mike." 125 | poetry run mike deploy -u ${{ github.ref_name }} latest 126 | poetry run mike set-default latest 127 | fi 128 | 129 | - name: Build and deploy documentation to gh-pages 130 | run: | 131 | SET_LATEST="" 132 | if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+*$ ]]; then 133 | # if a new release tag is pushed, mark the built documentation as 'latest' 134 | SET_LATEST="latest" 135 | fi 136 | poetry run mike deploy -u --push ${{ github.ref_name }} $SET_LATEST 137 | 138 | # ---- 139 | 140 | deploy_demo: 141 | needs: docs 142 | if: startswith(github.ref, 'refs/tags/v') 143 | secrets: inherit 144 | uses: "./.github/workflows/deploy_demo.yml" 145 | 146 | publish_template: 147 | # if a version tag is pushed + tests + docs completed -> do release 148 | needs: deploy_demo 149 | if: startswith(github.ref, 'refs/tags/v') 150 | permissions: 151 | contents: write # for GitHub release 152 | id-token: write # for PyPI release 153 | 154 | uses: "./.github/workflows/release.yml" 155 | with: 156 | to_github: true 157 | to_pypi: true 158 | to_test_pypi: false 159 | -------------------------------------------------------------------------------- /.github/workflows/deploy_demo.yml: -------------------------------------------------------------------------------- 1 | name: Deploy the demo repository 2 | 3 | on: [workflow_dispatch, workflow_call] 4 | 5 | jobs: 6 | deploy: 7 | if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' 8 | 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | - uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.10" 15 | 16 | - name: Set git author (needed for commits) 17 | run: | 18 | git config --global user.email "deploy-fair-python-cookiecutter@example.com" 19 | git config --global user.name "Deployment Bot" 20 | git config --global init.defaultBranch main 21 | 22 | - name: Prepare SSH (needed for remote push) 23 | uses: webfactory/ssh-agent@v0.7.0 24 | with: 25 | # NOTE: generated private key, stored repo secret 26 | # (matching public key is registered Deployment Key in target Github repo) 27 | ssh-private-key: ${{ secrets.SSH_DEPLOY_KEY }} 28 | 29 | # ---- 30 | 31 | - name: Checkout template repo 32 | uses: actions/checkout@v3 33 | with: 34 | path: fair-python-cookiecutter 35 | 36 | - name: Get current template version 37 | run: | 38 | TEMPLATE_DIR="./fair-python-cookiecutter" 39 | 40 | QUOTED_VERSION=$(grep -e "^version" $TEMPLATE_DIR/pyproject.toml | sed 's/^.*=//') 41 | TEMPLATE_VERSION=$(echo "$QUOTED_VERSION" | sed 's/"//g' | xargs) 42 | echo "extracted template version:" $TEMPLATE_VERSION 43 | 44 | echo "FAIR_TEMPLATE_DIR=$TEMPLATE_DIR" >> "$GITHUB_ENV" 45 | echo "FAIR_TEMPLATE_VERSION=$TEMPLATE_VERSION" >> "$GITHUB_ENV" 46 | 47 | - name: Prepare demo repository cookiecutter config 48 | run: | 49 | SED_CMD_PREF='s/\(^.*version:\).*/\1' 50 | SED_CMD="${SED_CMD_PREF} \"${FAIR_TEMPLATE_VERSION}\"/" 51 | TEMPLATE_CONFIG=cconf.yaml 52 | 53 | echo "running sed to insert version:" $SED_CMD 54 | sed "$SED_CMD" $FAIR_TEMPLATE_DIR/tests/demo.yaml > $TEMPLATE_CONFIG 55 | 56 | echo "resulting repo cookiecutter configuration:" 57 | cat $TEMPLATE_CONFIG 58 | 59 | echo "FAIR_TEMPLATE_CONFIG=$TEMPLATE_CONFIG" >> "$GITHUB_ENV" 60 | 61 | # ---- 62 | 63 | - name: Install poetry 64 | run: pipx install poetry 65 | 66 | - name: Install fair-python-cookiecutter (from local copy) 67 | run: pip install "./$FAIR_TEMPLATE_DIR" 68 | 69 | - name: Instantiate demo repository 70 | run: fair-python-cookiecutter --no-input --config-file=$FAIR_TEMPLATE_CONFIG --repo-url=https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter-demo 71 | 72 | # ---- 73 | 74 | - name: Get branch triggering the workflow 75 | run: | 76 | BRANCH="$(echo ${GITHUB_REF##*/})" 77 | if [[ $BRANCH == v*.*.* ]]; then 78 | BRANCH=main # tagged release -> main branch 79 | fi 80 | echo "ci branch:" $BRANCH 81 | echo "CI_BRANCH=$BRANCH" >> "$GITHUB_ENV" 82 | 83 | - name: Deploy generated repository 84 | run: | 85 | cd fair-python-cookiecutter-demo 86 | git remote add origin git@github.com:Materials-Data-Science-and-Informatics/fair-python-cookiecutter-demo.git 87 | 88 | if [ "$CI_BRANCH" != "main" ]; then 89 | # rename branch to desired target 90 | git branch -m main $CI_BRANCH 91 | git status 92 | fi 93 | 94 | git push -f --set-upstream origin $CI_BRANCH 95 | 96 | - name: Push tag (to trigger release workflow in demo) 97 | if: startswith(github.ref, 'refs/tags/v') 98 | run: | 99 | cd fair-python-cookiecutter-demo 100 | git tag "v$FAIR_TEMPLATE_VERSION" 101 | git push --tags origin $CI_BRANCH 102 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | # Release a new version to different targets in suitable ways 3 | 4 | on: 5 | workflow_call: # called from ci.yml 6 | inputs: 7 | to_github: 8 | description: "Create a Github Release (repository snapshot)" 9 | type: boolean 10 | default: true 11 | 12 | to_test_pypi: 13 | description: "Publish to Test PyPI." 14 | type: boolean 15 | default: false 16 | 17 | to_pypi: 18 | description: "Publish to PyPI." 19 | type: boolean 20 | default: false 21 | 22 | 23 | jobs: 24 | 25 | github: 26 | if: inputs.to_github 27 | name: Create a Github Release (repository snapshot) 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write # needed for creating a GH Release 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: softprops/action-gh-release@v1 34 | 35 | pypi: 36 | if: inputs.to_pypi || inputs.to_test_pypi 37 | name: Publish to PyPI (and/or compatible repositories) 38 | runs-on: ubuntu-latest 39 | permissions: 40 | id-token: write # needed for "trusted publishing" protocol 41 | steps: 42 | - uses: actions/checkout@v3 43 | 44 | - name: Install poetry 45 | run: pipx install poetry 46 | 47 | - uses: actions/setup-python@v4 48 | with: 49 | python-version: "3.10" 50 | cache: "poetry" 51 | 52 | - name: Build the distribution package 53 | run: poetry build 54 | 55 | - name: Publish package to TestPyPI 56 | if: inputs.to_test_pypi 57 | uses: pypa/gh-action-pypi-publish@release/v1 58 | with: 59 | repository-url: https://test.pypi.org/legacy/ 60 | 61 | - name: Publish package to PyPI 62 | if: inputs.to_pypi 63 | uses: pypa/gh-action-pypi-publish@release/v1 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | temp/ 2 | scratch/ 3 | 4 | # don't add vscode stuff 5 | .vscode 6 | 7 | # Created by https://www.toptal.com/developers/gitignore/api/python 8 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 9 | 10 | ### Python ### 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | pytestdebug.log 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | doc/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | #poetry.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .env/ 120 | .venv/ 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | pythonenv* 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | 146 | # pytype static type analyzer 147 | .pytype/ 148 | 149 | # operating system-related files 150 | # file properties cache/storage on macOS 151 | *.DS_Store 152 | # thumbnail cache on Windows 153 | Thumbs.db 154 | 155 | # profiling data 156 | .prof 157 | 158 | 159 | # End of https://www.toptal.com/developers/gitignore/api/python 160 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'src/fair_python_cookiecutter/template/.*' # <-- added 2 | # ---- copy from template below ---- 3 | # See https://pre-commit.com for more information 4 | # See https://pre-commit.com/hooks.html for more hooks 5 | repos: 6 | # GH Actions 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: '0.31.2' 9 | hooks: 10 | - id: check-github-workflows 11 | 12 | # Code Quality 13 | - repo: https://github.com/astral-sh/ruff-pre-commit 14 | rev: v0.9.9 15 | hooks: 16 | - id: ruff 17 | types_or: [python, pyi, jupyter] 18 | args: [--fix] 19 | - id: ruff-format 20 | types_or: [python, pyi, jupyter] 21 | 22 | - repo: https://github.com/pre-commit/mirrors-mypy 23 | rev: 'v1.15.0' 24 | hooks: 25 | - id: mypy 26 | args: [--no-strict-optional, --ignore-missing-imports] 27 | # NOTE: you might need to add some deps here: 28 | additional_dependencies: [] 29 | 30 | # Metadata 31 | - repo: https://github.com/citation-file-format/cff-converter-python 32 | rev: '054bda51dbe278b3e86f27c890e3f3ac877d616c' 33 | hooks: 34 | - id: validate-cff 35 | - repo: https://github.com/fsfe/reuse-tool 36 | rev: 'v5.0.2' 37 | hooks: 38 | - id: reuse 39 | 40 | - repo: https://github.com/Materials-Data-Science-and-Informatics/somesy 41 | rev: 'v0.7.1' 42 | hooks: 43 | - id: somesy 44 | 45 | # Various general + format-specific helpers 46 | - repo: https://github.com/pre-commit/pre-commit-hooks 47 | rev: v5.0.0 48 | hooks: 49 | - id: check-symlinks 50 | - id: trailing-whitespace 51 | exclude: 'CITATION.cff' 52 | - id: mixed-line-ending 53 | args: [--fix=lf] 54 | - id: check-yaml 55 | exclude: 'mkdocs.yml' 56 | - id: check-toml 57 | - id: check-json 58 | - id: check-ast 59 | - id: debug-statements 60 | - id: check-merge-conflict 61 | - id: check-shebang-scripts-are-executable 62 | - id: check-added-large-files 63 | args: [--maxkb=10000] 64 | -------------------------------------------------------------------------------- /.somesy.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "fair-python-cookiecutter" 3 | version = "1.0.0" 4 | description = "An opinionated cookiecutter template to kickstart a modern best-practice Python project with FAIR metadata. " 5 | 6 | license = "MIT" 7 | keywords = [ 8 | "fair", 9 | "metadata", 10 | "python", 11 | "cookiecutter", 12 | "template", 13 | ] 14 | 15 | repository = "https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter" 16 | homepage = "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter" 17 | documentation = "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter" 18 | 19 | [[project.people]] 20 | given-names = "Anton" 21 | family-names = "Pirogov" 22 | email = "a.pirogov@fz-juelich.de" 23 | orcid = "https://orcid.org/0000-0002-5077-7497" 24 | 25 | author = true 26 | 27 | [[project.people]] 28 | family-names = "Soylu" 29 | given-names = "Mustafa" 30 | email = "m.soylu@fz-juelich.de" 31 | orcid = "https://orcid.org/0000-0003-2637-0432" 32 | 33 | author = true 34 | maintainer = true 35 | 36 | contribution = "Supported implementation of Windows support and testing the template" 37 | contribution_types = ["code", "test"] 38 | 39 | [config] 40 | verbose = true 41 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors and Contributors 2 | 3 | **Main authors** are persons whose contributions significantly shaped 4 | the state of the software at some point in time. 5 | 6 | **Additional contributors** are persons who are not main authors, 7 | but contributed non-trivially to this project, 8 | e.g. by providing smaller fixes and enhancements to the code and/or documentation. 9 | 10 | Of course, this is just a rough overview and categorization. 11 | For a more complete overview of all contributors and contributions, 12 | please inspect the git history of this repository. 13 | 14 | ## Main Authors 15 | 16 | - Anton Pirogov ( 17 | [E-Mail](mailto:a.pirogov@fz-juelich.de), 18 | [ORCID](https://orcid.org/0000-0002-5077-7497) 19 | ): original author 20 | 21 | ## Additional Contributors 22 | 23 | 27 | 28 | ... maybe **[you](https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter/main/contributing)**? 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Here we provide notes that summarize the most important changes in each released version. 4 | 5 | Please consult the changelog to inform yourself about breaking changes and security issues. 6 | 7 | ## [v0.3.2](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/tree/v0.3.2) (2024-08-01) { id="0.3.2" } 8 | 9 | - updated `somesy` pre-commit hook to version `v0.4.3` 10 | - updated project and template dependencies 11 | - fixed python file imports 12 | - updated readme with example code 13 | - updated Contributing document issue 14 | 15 | ## [v0.3.1](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/tree/v0.3.1) (2024-07-10) { id="0.3.1" } 16 | 17 | - update pre-commit hooks to the latest version 18 | 19 | ## [v0.3.0](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/tree/v0.3.0) (2024-02-27) { id="0.3.0" } 20 | 21 | - added support for Windows 22 | - fixed a bug where instantiation of template fails if no repository URL is provided 23 | - improve usability and description of some CLI fields 24 | 25 | ## [v0.2.0](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/tree/v0.2.0) (2024-01-05) { id="0.2.0" } 26 | 27 | - Refactored into custom tool `fair-python-cookiecutter` for more versatility and convenience 28 | 29 | ## [v0.1.0](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/tree/v0.1.0) (2023-07-24) { id="0.1.0" } 30 | 31 | - First release 32 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | type: software 3 | message: If you use this software, please cite it using this metadata. 4 | 5 | title: "fair-python-cookiecutter" 6 | version: "1.0.0" 7 | abstract: "An opinionated cookiecutter template to kickstart a modern best-practice 8 | Python project with FAIR metadata." 9 | repository-code: "https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter" 10 | license: "MIT" 11 | keywords: 12 | - fair 13 | - metadata 14 | - python 15 | - cookiecutter 16 | - template 17 | authors: 18 | - family-names: Pirogov 19 | given-names: Anton 20 | email: a.pirogov@fz-juelich.de 21 | orcid: https://orcid.org/0000-0002-5077-7497 22 | affiliation: Forschungszentrum Jülich GmbH - Institute for Materials Data Science 23 | and Informatics (IAS9) 24 | - email: m.soylu@fz-juelich.de 25 | family-names: Soylu 26 | given-names: Mustafa 27 | orcid: https://orcid.org/0000-0003-2637-0432 28 | contact: 29 | - given-names: Mustafa 30 | orcid: https://orcid.org/0000-0003-2637-0432 31 | email: m.soylu@fz-juelich.de 32 | family-names: Soylu 33 | url: 34 | https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/CODE_OF_CONDUCT.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | All kinds of contributions are very welcome! 4 | You can contribute in various ways, e.g. by 5 | 6 | - providing feedback 7 | - asking questions 8 | - suggesting ideas 9 | - implementing features 10 | - fixing problems 11 | - improving documentation 12 | 13 | To make contributing to open source projects a good experience to everyone involved, 14 | please make sure that you follow our code of conduct when communicating with others. 15 | 16 | ## Ideas, Questions and Problems 17 | 18 | If you have questions or difficulties using this software, 19 | please use the [issue tracker](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/issues). 20 | 21 | If your topic is not already covered by an existing issue, 22 | please create a new issue using one of the provided issue templates. 23 | 24 | If your issue is caused by incomplete, unclear or outdated documentation, 25 | we are also happy to get suggestions on how to improve it. 26 | Outdated or incorrect documentation is a _bug_, 27 | while missing documentation is a _feature request_. 28 | 29 | **NOTE:** If you want to report a critical security problem, _do not_ open an issue! 30 | Instead, please create a [private security advisory](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability), 31 | or contact the current package maintainers directly by e-mail. 32 | 33 | ## Development 34 | 35 | This project uses [Poetry](https://python-poetry.org/) for dependency management. 36 | 37 | You can run the following lines to check out the project and prepare it for development: 38 | 39 | ``` 40 | git clone https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter.git 41 | cd fair-python-cookiecutter 42 | poetry install --with docs 43 | poetry run poe init-dev 44 | ``` 45 | 46 | Common tasks are accessible via [poe](https://github.com/nat-n/poethepoet): 47 | 48 | - Use `poetry run poe lint` to run linters manually, add `--all-files` to check everything. 49 | 50 | - Use `poetry run poe test` to run tests, add `--cov` to also show test coverage. 51 | 52 | - Use `poetry run poe docs` to generate local documentation 53 | 54 | In order to contribute code, please open a pull request. 55 | 56 | Before opening the PR, please make sure that your changes 57 | 58 | - are sufficiently covered by meaningful **tests**, 59 | - are reflected in suitable **documentation** (API docs, guides, etc.), and 60 | - successfully pass all pre-commit hooks. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Forschungszentrum Jülich GmbH - Institute for Materials Data Science and Informatics (IAS9) - Stefan Sandfeld 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [ 2 | ![Docs](https://img.shields.io/badge/read-docs-success) 3 | ](https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter) 4 | [ 5 | ![CI](https://img.shields.io/github/actions/workflow/status/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/ci.yml?branch=main&label=ci) 6 | ](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/actions/workflows/ci.yml) 7 | 8 | 9 | 10 |
11 |
12 | FAIR Python Cookiecutter Logo 13 |    14 |
15 |
16 | 17 | # fair-python-cookiecutter 18 | 19 | An opinionated cookiecutter template to kickstart a modern best-practice Python project with FAIR metadata. 20 | 21 | 23 | 24 | ![FAIR Software Mindmap](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/assets/371708/ef566ee1-8965-4d58-9ede-18eeb49a476e) 25 | 26 | _Check out the 27 | [demo repository](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter-demo) 28 | generated from this template!_ 29 | 30 | ## Overview 31 | 32 | Are you a **researcher** or **research software engineer**? 33 | 34 | Did you somehow end up developing **Python tools and libraries** as part of your job? 35 | 36 | Are you overwhelmed and confused by the increasing demands to research software? 37 | 38 | Regardless whether you are just planning to start a new software project, or you just 39 | look for ideas about how you could improve its quality - **this template is for you!** 40 | 41 | Unlike myriads of other templates, this template targets the typical case in academia - 42 | you built some nice little tool or library for your scientific community, 43 | and hope that others have a **good experience** - you want to provide a **quality 44 | product** that others enjoy using. In case they actually do use it with some success, you 45 | might also like to be acknowledged - for example, by having your tool **cited**. 46 | 47 | To ensure quality, there are many **best practices** and recommendations for **software 48 | development** on various levels, both general as well as Python-specific. To help others 49 | find your project and also enable them to cite it, there are also recommendations 50 | concerning your software project **metadata**. In fact, there are so many recommendations 51 | that it can be hard to keep up and easy to become overwhelmed and confused. 52 | 53 | To save you some time navigating all of that advice and figuring out how to apply it in 54 | practice, we did the work for you and provide you with this template! 55 | You can use it as is, adapt it, or at least get some inspiration for your projects. 56 | 57 | ## Main Features 58 | 59 | This template sets up a skeleton for a Python project that: 60 | 61 | - uses modern state-of-the-art development tools 62 | - provides a baseline for professional development and maintenance 63 | - helps following best practices for code and metadata quality 64 | - contains detailed documentation on how to work with it 65 | 66 | It is built to help you adopting good practices 67 | and follow recommendations such as: 68 | 69 | - [DLR Software Engineering Guidelines](https://rse.dlr.de/guidelines/00_dlr-se-guidelines_en.html) 70 | - [OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/en/criteria/0) 71 | - [Netherlands eScience Center](https://fair-software.eu) 72 | - innumerable other resources that can be found online 73 | 74 | Furthermore, it implements emerging standards with the goal to improve 75 | software metadata and make it more [FAIR](https://www.go-fair.org/fair-principles/): 76 | 77 | - [REUSE](https://reuse.software/) 78 | - [CITATION.cff](https://citation-file-format.github.io/) 79 | - [CodeMeta](https://codemeta.github.io/) 80 | 81 | Also see [this paper](https://doi.org/10.48550/arXiv.1905.08674) for an overview and 82 | recommendations on the state of software citation in academic practice. 83 | 84 | 85 | 86 | 87 | 88 | ## Getting Started 89 | 90 | Make sure that you have a working Python interpreter in version at least 3.9, 91 | `git` and [`poetry`](https://python-poetry.org/docs/#installation) installed. 92 | 93 | To install the template, run `pip install fair-python-cookiecutter`. 94 | 95 | Now you can use the tool to generate a new Python project: 96 | 97 | ```bash 98 | fair-python-cookiecutter YourProjectName 99 | ``` 100 | 101 | This will spawn an interactive prompt, where you have to provide some information and 102 | make some choices for your new software project. Don't worry, you can always adapt 103 | everything later on by hand. After this, your software project will be created in 104 | a new directory. 105 | 106 | To save you some time answering the questions, we recommend that you create an empty repository 107 | in GitHub or GitLab of your choice (i.e., the location where you plan to push your new project). 108 | 109 | If you already have created an empty remote repository or know exactly its future 110 | location, you can provide the URL, which already will provide many required inputs: 111 | 112 | ```bash 113 | fair-python-cookiecutter --repo-url https://github.com/YourOrganization/YourProjectName 114 | ``` 115 | 116 | Your new project repository will also include a copy of a 117 | [developer guide](https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter-demo/latest/dev_guide), 118 | containing more information about the structure and features of the generated project. 119 | 120 | **Please familiarize yourself with the generated structures, files and the contents of the 121 | developer guide.** Feel free to either remove the guide afterwards, or keep (and possibly 122 | adjust) it as extended technical project documentation for yourself and other future 123 | project contributors. 124 | 125 | You can find a demo repository generated from this template [here](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter-demo). 126 | 127 | ### Example Code 128 | 129 | When you are creating your project, you are asked for several inputs. You can have an example CLI(Command Line Interface) and/or API(Application Programming interface) code within your new project. 130 | 131 | Lets assume your project name is `my-awesome-project`. 132 | 133 | You can run the CLI App with below command. For further usage, please check typer documentation. 134 | 135 | ```bash 136 | poetry shell 137 | 138 | # Run your CLI App 139 | my-awesome-project-cli calculate add 5 2 140 | ``` 141 | 142 | You can run the API App with below command. 143 | 144 | ```bash 145 | poetry shell 146 | 147 | # Run the API program, it will be open for connection 148 | my-awesome-project-api 149 | ``` 150 | 151 | Now, you can a tool to send a HTTP request for the API. You can open another terminal and run this command 152 | 153 | ``` 154 | # send a request that does the same thing as the CLI 155 | curl 'http://localhost:8000/calculate/add?x=5&y=2' 156 | ``` 157 | 158 | For further usage of the API, please check fastAPI documentation. 159 | 160 | ## Configuring the Template 161 | 162 | If you intend to use the template a lot, e.g. if you want to use (an adaptation of) 163 | this template as the default way to start a Python project for yourself and/or others, 164 | you might want to configure some template variables in your `~/.cookiecutterrc`. 165 | Here is an example cookiecutter configuration: 166 | 167 | ```yaml 168 | fair_python_cookiecutter: 169 | last_name: 'Carberry' 170 | first_name: 'Josiah' 171 | project_keywords: 'psychoceramics analytics' 172 | email: 'josiah.carberry@brown.edu' 173 | orcid: '0000-0002-1825-0097' 174 | affiliation: 'Brown University' 175 | copyright_holder: 'Brown University' 176 | license: 'MIT' 177 | ``` 178 | 179 | This information will be already pre-filled when you use the template, 180 | saving you some time and possibly avoiding possible mistakes from manual typing. 181 | 182 | ## Modifying the Template 183 | 184 | If you want to adjust it to your needs and likings (e.g. add, remove or substitute certain 185 | tools), you probably want to 186 | [fork](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/fork) 187 | it to get your own copy. Then you can do the desired changes and use the URL of your 188 | template repository instead of this one to kickstart your projects. 189 | 190 | However, if you think that your changes are of general interest and would improve this 191 | template for a majority of users, please get in touch 192 | and [contribute](https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter/main/contributing/) 193 | or [suggest](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/issues) an improvement! 194 | 195 | In any case we are very happy to know about any similar or derivative templates, e.g. for 196 | more specific use-cases or based on other tool preferences. 197 | 198 | ## Reusing Parts of the Template 199 | 200 | If you already have an existing project where you would like to introduce things you like 201 | from this template, there are two main ways to do so: 202 | 203 | 1. move your code into a fresh repository based on this template 204 | 2. use parts of the template in your existing project structure 205 | 206 | If your project currently has no sophisticated setup of tools or strong preferences about 207 | them, option 1 might be the simplest way to adopt the template. Your code then needs to be 208 | moved into the `YOUR_PROJECT/src/YOUR_PACKAGE` subdirectory. 209 | 210 | On the other hand, if you already have a working setup that you do not wish to replace 211 | completely, you can take a look at 212 | 213 | - the `.pre-commit-config.yaml` file to adopt some of the quality assurance tools listed there 214 | - the CI pipelines defined in `.github/workflows` or `.gitlab-ci.yml` for automated tests and releases 215 | - the `mkdocs.yml` and `docs/` subdirectory to see how the project website works 216 | 217 | 218 | 219 | 220 | 221 | ## How to Cite 222 | 223 | If you want to cite this project in your scientific work, 224 | please use the [citation file](https://citation-file-format.github.io/) 225 | in the [repository](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter/blob/main/CITATION.cff). 226 | 227 | 228 | 229 | 230 | ## Acknowledgements 231 | 232 | We kindly thank all 233 | [authors and contributors](https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter/latest/credits). 234 | 235 |
236 | HMC Logo 237 |    238 | FZJ Logo 239 |
240 |
241 | 242 | This project was developed at the Institute for Materials Data Science and Informatics 243 | (IAS-9) of the Jülich Research Center and funded by the Helmholtz Metadata Collaboration 244 | (HMC), an incubator-platform of the Helmholtz Association within the framework of the 245 | Information and Data Science strategic initiative. 246 | 247 | 248 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "fair-python-cookiecutter" 3 | SPDX-PackageSupplier = "Anton Pirogov " 4 | SPDX-PackageDownloadLocation = "https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter" 5 | 6 | [[annotations]] 7 | path = ["{{ cookiecutter**"] 8 | precedence = "override" 9 | SPDX-FileCopyrightText = "2023 Forschungszentrum Jülich GmbH - Institute for Materials Data Science and Informatics (IAS9) - Stefan Sandfeld " 10 | SPDX-License-Identifier = "CC0-1.0" 11 | 12 | [[annotations]] 13 | path = [".gitignore", "pyproject.toml", "poetry.lock", ".pre-commit-config.yaml", ".somesy.toml", "codemeta.json", "CITATION.cff", "README.md", "RELEASE_NOTES.md", "CHANGELOG.md", "CODE_OF_CONDUCT.md", "AUTHORS.md", "CONTRIBUTING.md", ".github/**", "mkdocs.yml", "**.txt", "docs/**", "REUSE.toml", "somesy.toml"] 14 | precedence = "override" 15 | SPDX-FileCopyrightText = "2023 Forschungszentrum Jülich GmbH - Institute for Materials Data Science and Informatics (IAS9) - Stefan Sandfeld " 16 | SPDX-License-Identifier = "CC0-1.0" 17 | 18 | [[annotations]] 19 | path = ["src/**", "tests/**", "hooks/**"] 20 | precedence = "override" 21 | SPDX-FileCopyrightText = "2023 Forschungszentrum Jülich GmbH - Institute for Materials Data Science and Informatics (IAS9) - Stefan Sandfeld " 22 | SPDX-License-Identifier = "MIT" 23 | -------------------------------------------------------------------------------- /codemeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://doi.org/10.5063/schema/codemeta-2.0", 4 | "https://w3id.org/software-iodata", 5 | "https://raw.githubusercontent.com/jantman/repostatus.org/master/badges/latest/ontology.jsonld", 6 | "https://schema.org", 7 | "https://w3id.org/software-types" 8 | ], 9 | "@type": "SoftwareSourceCode", 10 | "author": [ 11 | { 12 | "@type": "Person", 13 | "givenName": "Anton", 14 | "familyName": "Pirogov", 15 | "email": "a.pirogov@fz-juelich.de", 16 | "@id": "https://orcid.org/0000-0002-5077-7497", 17 | "identifier": "https://orcid.org/0000-0002-5077-7497" 18 | }, 19 | { 20 | "@type": "Person", 21 | "givenName": "Mustafa", 22 | "familyName": "Soylu", 23 | "email": "m.soylu@fz-juelich.de", 24 | "@id": "https://orcid.org/0000-0003-2637-0432", 25 | "identifier": "https://orcid.org/0000-0003-2637-0432" 26 | } 27 | ], 28 | "name": "fair-python-cookiecutter", 29 | "description": "An opinionated cookiecutter template to kickstart a modern best-practice Python project with FAIR metadata.", 30 | "version": "1.0.0", 31 | "keywords": [ 32 | "fair", 33 | "metadata", 34 | "python", 35 | "cookiecutter", 36 | "template" 37 | ], 38 | "maintainer": [ 39 | { 40 | "@type": "Person", 41 | "givenName": "Mustafa", 42 | "familyName": "Soylu", 43 | "email": "m.soylu@fz-juelich.de", 44 | "@id": "https://orcid.org/0000-0003-2637-0432", 45 | "identifier": "https://orcid.org/0000-0003-2637-0432" 46 | } 47 | ], 48 | "license": [ 49 | "https://spdx.org/licenses/MIT" 50 | ], 51 | "softwareHelp": "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter", 52 | "codeRepository": "https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter", 53 | "buildInstructions": "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter", 54 | "url": "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter" 55 | } -------------------------------------------------------------------------------- /fair_sw_mermaid.txt: -------------------------------------------------------------------------------- 1 | mindmap 2 | root(("FAIR Software")) 3 | (Development Practices) 4 | (Documentation) 5 | (Users) 6 | (Installation) 7 | (Examples) 8 | (Tutorials) 9 | (Developers) 10 | (API Docs) 11 | (Workflows) 12 | (Testing) 13 | (Unit Tests) 14 | (Test Coverage) 15 | ("..") 16 | ("Maintenance /
DevOps") 17 | (Security Scans) 18 | ci("CI/CD") 19 | (Releases) 20 | (General) 21 | (FLOSS Licensing) 22 | (Code of Conduct) 23 | (Issue Templates) 24 | (Project Website) 25 | ("Guidelines /
Standards") 26 | (Development) 27 | [OpenSSF] 28 | [DLR] 29 | [FZJ] 30 | (Metadata) 31 | [FAIR Principles] 32 | [HMC Guidance] 33 | [Citation File] 34 | [CodeMeta] 35 | [REUSE] 36 | (Tools) 37 | (General) 38 | pc["pre - commit"] 39 | [REUSE tool] 40 | [cffconvert] 41 | [codemetapy] 42 | [somesy] 43 | (Python) 44 | [poetry] 45 | [pytest] 46 | [mypy] 47 | [mkdocs] 48 | [ruff] 49 | [".."] 50 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # basic configuration: 2 | site_name: "fair-python-cookiecutter" 3 | site_description: "An opinionated cookiecutter template to kickstart a modern best-practice 4 | Python project with FAIR metadata." 5 | repo_name: "Materials-Data-Science-and-Informatics/fair-python-cookiecutter" 6 | 7 | site_url: "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter" 8 | repo_url: "https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter" 9 | edit_uri: "edit/main/docs/" 10 | 11 | docs_dir: "src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs" 12 | watch: ["README.md"] 13 | 14 | # navigation structure (TOC + respective markdown files): 15 | nav: 16 | - Home: 17 | - Overview: index.md 18 | - Changelog: changelog.md 19 | - Credits: credits.md 20 | - License: license.md 21 | - Usage: 22 | - Quickstart: quickstart.md 23 | - Development: 24 | - How To Contribute: contributing.md 25 | - Code of Conduct: code_of_conduct.md 26 | 27 | extra: 28 | # social links in footer: 29 | social: 30 | - icon: 'fontawesome/brands/github' 31 | link: 'https://github.com/Materials-Data-Science-and-Informatics' 32 | - icon: 'fontawesome/solid/globe' 33 | link: 'https://github.com/Materials-Data-Science-and-Informatics' 34 | 35 | # versioned docs: https://squidfunk.github.io/mkdocs-material/setup/setting-up-versioning/ 36 | version: 37 | provider: mike 38 | 39 | # optimization for offline usage: 40 | use_directory_urls: !ENV [OFFLINE, false] 41 | 42 | theme: 43 | # See here for customization guide: https://squidfunk.github.io/mkdocs-material/setup/ 44 | name: "material" 45 | custom_dir: "src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug 46 | }}/docs/overrides" 47 | 48 | features: 49 | - content.action.edit 50 | - content.action.view 51 | # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations 52 | - content.code.annotate 53 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-header/ 54 | - header.autohide 55 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/ 56 | - navigation.footer 57 | - navigation.instant 58 | - navigation.tabs 59 | - navigation.tabs.sticky 60 | - navigation.tracking 61 | - navigation.top 62 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/ 63 | - search.highlight 64 | - search.suggest 65 | 66 | # light/dark mode toggle: https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ 67 | palette: 68 | - media: "(prefers-color-scheme: light)" 69 | scheme: default 70 | toggle: 71 | icon: material/brightness-7 72 | name: Switch to dark mode 73 | - media: "(prefers-color-scheme: dark)" 74 | scheme: slate 75 | toggle: 76 | icon: material/brightness-4 77 | name: Switch to light mode 78 | 79 | extra_css: 80 | # list of extra CSS files to override and configure defaults: 81 | - css/style.css 82 | 83 | markdown_extensions: 84 | # Enable permalink to sections: 85 | - toc: 86 | permalink: true 87 | # Setting HTML/CSS attributes: https://python-markdown.github.io/extensions/attr_list/ 88 | - attr_list 89 | # Definitions: https://python-markdown.github.io/extensions/definition_lists/ 90 | - def_list 91 | # Footnotes: https://squidfunk.github.io/mkdocs-material/reference/footnotes/ 92 | - footnotes 93 | # Various boxes: https://squidfunk.github.io/mkdocs-material/reference/admonitions/ 94 | - admonition 95 | - pymdownx.details 96 | - pymdownx.superfences 97 | # smart links: https://facelessuser.github.io/pymdown-extensions/extensions/magiclink/ 98 | - pymdownx.magiclink: 99 | repo_url_shorthand: true 100 | # Superscript: https://facelessuser.github.io/pymdown-extensions/extensions/caret/ 101 | - pymdownx.caret 102 | # Strikethrough markup: https://facelessuser.github.io/pymdown-extensions/extensions/tilde/ 103 | - pymdownx.tilde 104 | # Auto-Unicode for common symbols: https://facelessuser.github.io/pymdown-extensions/extensions/smartsymbols/ 105 | - pymdownx.smartsymbols 106 | # Github-style task list: https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#tasklist 107 | - pymdownx.tasklist: 108 | custom_checkbox: true 109 | # Tabbed boxes: https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ 110 | - pymdownx.tabbed: 111 | alternate_style: true 112 | # Inlining markdown: https://facelessuser.github.io/pymdown-extensions/extensions/snippets/ 113 | - pymdownx.snippets: 114 | check_paths: true 115 | # Icons and Emoji: https://squidfunk.github.io/mkdocs-material/reference/icons-emojis/ 116 | - pymdownx.emoji: 117 | emoji_index: !!python/name:material.extensions.emoji.twemoji 118 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 119 | 120 | plugins: 121 | # default search box (must be listed if plugins are added) 122 | - search 123 | # execute code (e.g. generate diagrams): https://pawamoy.github.io/markdown-exec/ 124 | - markdown-exec 125 | # automatic API docs: https://mkdocstrings.github.io/recipes/#automatic-code-reference-pages 126 | - literate-nav: 127 | nav_file: SUMMARY.md 128 | - section-index 129 | - autorefs 130 | - mkdocstrings: 131 | handlers: 132 | python: 133 | paths: [src] 134 | options: 135 | members_order: source 136 | separate_signature: true 137 | show_signature_annotations: true 138 | # https://squidfunk.github.io/mkdocs-material/setup/building-for-offline-usage/#built-in-offline-plugin 139 | # To allow building for offline usage, e.g. with: OFFLINE=true mkdocs build 140 | - offline: 141 | enabled: !ENV [OFFLINE, false] 142 | # to make multi-version docs work right 143 | - mike 144 | 145 | strict: true 146 | site_author: Anton Pirogov 147 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # ---- managed by somesy, see .somesy.toml ---- 2 | [project] 3 | name = "fair-python-cookiecutter" 4 | version = "1.0.0" 5 | description = "An opinionated cookiecutter template to kickstart a modern best-practice Python project with FAIR metadata." 6 | readme = "README.md" 7 | keywords = [ 8 | "fair", 9 | "metadata", 10 | "python", 11 | "cookiecutter", 12 | "template", 13 | ] 14 | 15 | classifiers = [ 16 | "Intended Audience :: Science/Research", 17 | "Intended Audience :: Developers", 18 | ] 19 | requires-python = ">=3.9" 20 | dependencies = [ 21 | "cookiecutter>=2.6.0", 22 | "typer[all]>=0.12.3", 23 | "pydantic>=2.8.2", 24 | "typing-extensions>=4.12.2", 25 | "pydantic-yaml>=1.3.0", 26 | "spdx-lookup>=0.3.3", 27 | "platformdirs>=4.2.2", 28 | "importlib-resources>=6.4.0", 29 | "importlib-metadata (>=8.6.1,<9.0.0)", 30 | ] 31 | authors = [ 32 | {name = "Anton Pirogov",email = "a.pirogov@fz-juelich.de"}, 33 | {name = "Mustafa Soylu",email = "m.soylu@fz-juelich.de"}, 34 | ] 35 | maintainers = [ 36 | {name = "Mustafa Soylu",email = "m.soylu@fz-juelich.de"}, 37 | ] 38 | 39 | license = {text = "MIT"} 40 | 41 | [project.urls] 42 | homepage = "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter" 43 | repository = "https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter" 44 | documentation = "https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter" 45 | 46 | [tool.poetry] 47 | # the Python packages that will be included in a built distribution: 48 | packages = [{include = "fair_python_cookiecutter", from = "src"}] 49 | 50 | # always include basic info for humans and core metadata in the distribution, 51 | # include files related to test and documentation only in sdist: 52 | include = [ 53 | "*.md", "LICENSE", "LICENSES", "REUSE.toml", "CITATION.cff", "codemeta.json", 54 | "mkdocs.yml", "docs", "tests", 55 | { path = "mkdocs.yml", format = "sdist" }, 56 | { path = "docs", format = "sdist" }, 57 | { path = "tests", format = "sdist" }, 58 | ] 59 | 60 | [tool.poetry.group.dev.dependencies] 61 | poethepoet = "^0.32.2" 62 | pre-commit = "^3.5.0" 63 | pytest = "^8.3.2" 64 | 65 | [tool.poetry.group.docs] 66 | optional = true 67 | 68 | [tool.poetry.group.docs.dependencies] 69 | mkdocs = "^1.6.1" 70 | mkdocstrings = {extras = ["python"], version = "^0.25.2"} 71 | mkdocs-material = "^9.5.30" 72 | mkdocs-gen-files = "^0.5.0" 73 | mkdocs-literate-nav = "^0.6.1" 74 | mkdocs-section-index = "^0.3.9" 75 | mkdocs-macros-plugin = "^1.0.5" 76 | mkdocs-autorefs = "^1.4.0" 77 | markdown-include = "^0.8.1" 78 | pymdown-extensions = "^10.9" 79 | markdown-exec = {extras = ["ansi"], version = "^1.9.3"} 80 | mkdocs-coverage = "^1.1.0" 81 | mike = "^2.1.2" 82 | anybadge = "^1.14.0" 83 | interrogate = "^1.7.0" 84 | black = "^24.4.2" 85 | 86 | [project.scripts] 87 | fair-python-cookiecutter = "fair_python_cookiecutter.cli:app" 88 | 89 | [build-system] 90 | requires = ["poetry-core>=2.0"] 91 | build-backend = "poetry.core.masonry.api" 92 | 93 | # NOTE: You can run the following with "poetry poe TASK" 94 | [tool.poe.tasks] 95 | init-dev = { shell = "pre-commit install" } 96 | lint = "pre-commit run" # pass --all-files to check everything 97 | test = "pytest" # pass --cov to also collect coverage info 98 | docs = "mkdocs build" # run this to generate local documentation 99 | 100 | # Tool Configurations 101 | # ------------------- 102 | 103 | [tool.pytest.ini_options] 104 | pythonpath = ["src"] 105 | addopts = "--ignore='src/fair_python_cookiecutter/template'" 106 | # addopts = "--cov-report=term-missing:skip-covered" 107 | filterwarnings = [ 108 | "ignore::DeprecationWarning:pkg_resources.*", 109 | "ignore::DeprecationWarning:pyshacl.*", 110 | # Example: 111 | # "ignore::DeprecationWarning:importlib_metadata.*", 112 | ] 113 | 114 | [tool.coverage.run] 115 | source = ["fair_python_cookiecutter"] 116 | 117 | [tool.coverage.report] 118 | exclude_lines = [ 119 | "pragma: no cover", 120 | "def __repr__", 121 | "if self.debug:", 122 | "if settings.DEBUG", 123 | "raise AssertionError", 124 | "raise NotImplementedError", 125 | "if 0:", 126 | "if TYPE_CHECKING:", 127 | "if __name__ == .__main__.:", 128 | "class .*\\bProtocol\\):", 129 | "@(abc\\.)?abstractmethod", 130 | ] 131 | 132 | [tool.ruff.lint] 133 | # see here: https://docs.astral.sh/ruff/rules/ 134 | # ruff by default works like a mix of flake8, autoflake and black 135 | # we extend default linter rules to substitute: 136 | # flake8-bugbear, pydocstyle, isort, flake8-bandit 137 | extend-select = ["B", "D", "I", "S"] 138 | ignore = ["D203", "D213", "D407", "B008", "S101", "D102", "D103"] 139 | 140 | [tool.ruff.lint.per-file-ignores] 141 | "**/{tests,docs}/*" = ["ALL"] 142 | 143 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/__init__.py: -------------------------------------------------------------------------------- 1 | """somesy package.""" 2 | 3 | from typing import Final 4 | 5 | import importlib_metadata 6 | 7 | # Set version, it will use version from pyproject.toml if defined 8 | __version__: Final[str] = importlib_metadata.version(__package__ or __name__) 9 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/cli.py: -------------------------------------------------------------------------------- 1 | """Main entry point for the somesy CLI.""" 2 | 3 | import logging 4 | from pathlib import Path 5 | from typing import Optional 6 | 7 | import typer 8 | from rich import print 9 | from rich.panel import Panel 10 | from rich.prompt import Confirm 11 | from rich.rule import Rule 12 | from typing_extensions import Annotated 13 | 14 | from . import __version__ 15 | from .config import CookiecutterConfig, CookiecutterJson 16 | from .main import create_repository 17 | 18 | logger = logging.getLogger("fair_python_cookiecutter") 19 | 20 | app = typer.Typer() 21 | 22 | 23 | FIRST_TIME_NOTE = """[b]Welcome![/b] It looks like this is the first time you are using this tool. 24 | 25 | Before creating a new software repository, please answer some questions about your project. 26 | The information you provide is used to fine-tune the template to your needs and environment. 27 | 28 | After answering all questions you will have the option the save the answers for the next time.""" 29 | 30 | REPO_URL_NOTE = """[b]Tip:[/b] If you have already created an existing remote repository for this project on 31 | GitHub or a GitLab instance, you can provide the repository URL now ([b]recommended[/b]) 32 | to simplify the initialization process. Otherwise, just leave the following field empty.""" 33 | 34 | SAVE_QUESTION = """\n[i]Many of the values not specific to this project can be saved for next time, 35 | so you do not have to enter them again (but you will be asked to confirm).[/i] 36 | Do you want to save these settings?""" 37 | 38 | 39 | def infer_from_args( 40 | ccconf: CookiecutterConfig, repo_url: str = "", output_dir: Optional[Path] = None 41 | ): 42 | """Infer some values based on passed CLI arguments, returns output_dir value.""" 43 | if repo_url: 44 | ccconf.fair_python_cookiecutter.infer_from_repo_url(repo_url) 45 | if output_dir: 46 | ccconf.fair_python_cookiecutter.infer_from_output_dir(output_dir) 47 | return output_dir.parent 48 | return Path(".") 49 | 50 | 51 | def prompt_config(ccconf: CookiecutterConfig): 52 | """Prompt user to confirm or update all values set for template variables.""" 53 | # ask user for a repo url first if missing (can extract lots of info from it) 54 | repo_url = ccconf.fair_python_cookiecutter.project_repo_url 55 | if not repo_url: 56 | repo_url = ccconf.fair_python_cookiecutter.prompt_field("project_repo_url") 57 | # pre-fill default values based on remote repo URL, if provided 58 | if repo_url: 59 | ccconf.fair_python_cookiecutter.infer_from_repo_url(repo_url) 60 | # go through and confirm all the other fields, with loaded + inferred values 61 | ccconf.fair_python_cookiecutter.prompt_fields(exclude=["project_repo_url"]) 62 | # infer URL from provided information if it was not given at some point 63 | if not ccconf.fair_python_cookiecutter.project_repo_url: 64 | ccconf.fair_python_cookiecutter.infer_repo_url() 65 | 66 | 67 | @app.command() 68 | def main( 69 | dry_run: bool = False, 70 | no_input: bool = False, 71 | config_file: Annotated[ 72 | Optional[Path], typer.Option(file_okay=True, readable=True) 73 | ] = None, 74 | repo_url: str = "", 75 | output_dir: Annotated[ 76 | Optional[Path], 77 | typer.Argument( 78 | dir_okay=False, 79 | file_okay=False, 80 | resolve_path=True, 81 | ), 82 | ] = None, 83 | keep_project_on_failure: bool = False, 84 | ): 85 | """Create a new project from the template.""" 86 | print(Rule(title=f"[b]FAIR Python Cookiecutter[/b] {__version__}")) 87 | 88 | # load config (.cookiecutterrc if it exists, or defaults) 89 | ccconf = CookiecutterConfig.load(config_file=config_file) 90 | 91 | if ccconf.fair_python_cookiecutter.is_default(): 92 | # show info for new users 93 | print(Panel.fit(FIRST_TIME_NOTE)) 94 | print(Panel.fit(REPO_URL_NOTE)) 95 | 96 | # infer values 97 | output_dir = infer_from_args(ccconf, repo_url=repo_url, output_dir=output_dir) 98 | 99 | if not no_input: 100 | # confirm / complete values 101 | prompt_config(ccconf) 102 | 103 | if not dry_run and Confirm.ask(SAVE_QUESTION, default=False): 104 | # update config 105 | ccconf.save() 106 | print(f"\n[i]Your settings were saved in {ccconf.config_path()} ![/i]") 107 | 108 | # check values after all the tweaking 109 | ccconf.fair_python_cookiecutter.check() 110 | 111 | if dry_run: 112 | # show and exit 113 | print(f"output_dir={output_dir}", ccconf, CookiecutterJson.from_config(ccconf)) 114 | return 115 | 116 | # we're ready to create the repository 117 | create_repository(ccconf, output_dir, keep_on_fail=keep_project_on_failure) 118 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/config.py: -------------------------------------------------------------------------------- 1 | """Configuration data model and related utilities.""" 2 | 3 | from datetime import datetime 4 | from pathlib import Path 5 | from typing import Any, Dict, List, Optional, Union 6 | 7 | import spdx_lookup 8 | from cookiecutter.config import get_user_config 9 | from cookiecutter.extensions import pyslugify 10 | from pydantic import ( 11 | AfterValidator, 12 | BaseModel, 13 | BeforeValidator, 14 | ConfigDict, 15 | Field, 16 | HttpUrl, 17 | ValidationError, 18 | ) 19 | from pydantic_yaml import to_yaml_file 20 | from rich import print 21 | from rich.panel import Panel 22 | from rich.prompt import Confirm, Prompt 23 | from typing_extensions import Annotated, Literal, Self, get_args, get_origin 24 | 25 | from . import __version__ 26 | 27 | 28 | def to_spdx_license(s: str): 29 | if license := spdx_lookup.by_id(s): 30 | return license.id 31 | else: 32 | raise ValueError("Invalid SPDX license id!") 33 | 34 | 35 | TODAY = datetime.now() 36 | 37 | Email = Annotated[str, Field(pattern=r"^[\S]+@[\S]+\.[\S]+$")] 38 | Orcid = Annotated[ 39 | str, 40 | Field(pattern=r"^(\d{4}-){3}\d{3}(\d|X)$"), 41 | AfterValidator(lambda s: f"https://orcid.org/{s}"), 42 | ] 43 | OrcidUrl = Annotated[str, Field(pattern=r"^https://orcid.org/(\d{4}-){3}\d{3}(\d|X)$")] 44 | SemVerStr = Annotated[str, Field(pattern=r"^\d+\.\d+\.\d+$")] 45 | SPDXLicense = Annotated[str, AfterValidator(to_spdx_license)] 46 | MyHttpUrl = Annotated[ 47 | HttpUrl, 48 | BeforeValidator( 49 | lambda s: f"https://{s}" if s and not str(s).startswith("https://") else s 50 | ), 51 | ] 52 | 53 | 54 | class MyBaseModel(BaseModel): 55 | """Tweaked BaseModel to manage the template settings. 56 | 57 | Adds functionality to prompt user via CLI for values of fields. 58 | 59 | Assumes that all fields have either a default value (None is acceptable, 60 | even if the field it is not optional) or another nested model. 61 | 62 | This ensures that the object can be constructed without being complete yet. 63 | """ 64 | 65 | model_config = ConfigDict( 66 | str_strip_whitespace=True, str_min_length=1, validate_assignment=True 67 | ) 68 | 69 | def check(self): 70 | """Run validation on this object again.""" 71 | self.model_validate(self.model_dump()) 72 | 73 | @staticmethod 74 | def _unpack_annotation(ann): 75 | """Unpack an annotation from optional, raise exception if it is a non-trivial union.""" 76 | o, ts = get_origin(ann), get_args(ann) 77 | is_union = o is Union 78 | fld_types = [ann] if not is_union else [t for t in ts if t is not type(None)] 79 | 80 | ret = [] 81 | for t in fld_types: 82 | inner_kind = get_origin(t) 83 | if inner_kind is Literal: 84 | ret.append([a for a in get_args(t)]) 85 | elif inner_kind is Union: 86 | raise TypeError("Complex nested types are not supported!") 87 | else: 88 | ret.append(t) 89 | return ret 90 | 91 | def _field_prompt(self, key: str, *, required_only: bool = False): 92 | """Interactive prompt for one primitive field of the object (one-shot, no retries).""" 93 | fld = self.model_fields[key] 94 | val = getattr(self, key, None) 95 | 96 | if required_only and not fld.is_required(): 97 | return val 98 | 99 | defval = val or fld.default 100 | 101 | prompt_msg = f"\n[b]{key}[/b]" 102 | if fld.description: 103 | prompt_msg = f"\n[i]{fld.description}[/i]{prompt_msg}" 104 | 105 | ann = self._unpack_annotation(fld.annotation) 106 | fst, tail = ann[0], ann[1:] 107 | 108 | choices = fst if isinstance(fst, list) else None 109 | if fst is bool and not tail: 110 | defval = bool(defval) 111 | user_val = Confirm.ask(prompt_msg, default=defval) 112 | else: 113 | if not isinstance(defval, str) and defval is not None: 114 | defval = str(defval) 115 | user_val = Prompt.ask(prompt_msg, default=defval, choices=choices) 116 | 117 | setattr(self, key, user_val) # assign (triggers validation) 118 | return getattr(self, key) # return resulting parsed value 119 | 120 | def prompt_field( 121 | self, 122 | key: str, 123 | *, 124 | recursive: bool = True, 125 | missing_only: bool = False, 126 | required_only: bool = False, 127 | ) -> Any: 128 | """Interactive prompt for one field of the object. 129 | 130 | Will show field description to the user and pre-set the current value of the model as the default. 131 | 132 | The resulting value is validated and assigned to the field of the object. 133 | """ 134 | val = getattr(self, key) 135 | if isinstance(val, MyBaseModel): 136 | if recursive: 137 | val.prompt_fields( 138 | missing_only=missing_only, 139 | recursive=recursive, 140 | required_only=required_only, 141 | ) 142 | else: # no recursion -> just skip nested objects 143 | return 144 | 145 | # primitive case - prompt for value and retry if given invalid input 146 | while True: 147 | try: 148 | # prompt, parse and return resulting value 149 | return self._field_prompt(key, required_only=required_only) 150 | except ValidationError as e: 151 | print() 152 | print(Panel.fit(str(e))) 153 | print("[red]The provided value is not valid, please try again.[/red]") 154 | 155 | def prompt_fields( 156 | self, 157 | *, 158 | recursive: bool = True, 159 | missing_only: bool = False, 160 | required_only: bool = False, 161 | exclude: List[str] = None, 162 | ): 163 | """Interactive prompt for all fields of the object. See `prompt_field`.""" 164 | excluded = set(exclude or []) 165 | for key in self.model_fields.keys(): 166 | if missing_only and getattr(self, key, None) is None: 167 | continue 168 | if key not in excluded: 169 | self.prompt_field( 170 | key, 171 | recursive=recursive, 172 | missing_only=True, 173 | required_only=required_only, 174 | ) 175 | 176 | 177 | PAGES_DOMAINS = { 178 | "github.com": "github.io", 179 | "gitlab.com": "pages.gitlab.io", 180 | "codebase.helmholtz.cloud": "pages.hzdr.de", 181 | "gitlab.hzdr.de": "pages.hzdr.de", 182 | } 183 | 184 | 185 | class FPCConfig(MyBaseModel): 186 | """Our cookiecutter template configuration. 187 | 188 | Based on the config, a final cookiecutter directory (with cookiecutter.json etc.) 189 | is generated from the meta-template. 190 | """ 191 | 192 | def is_default(self) -> bool: 193 | """Return whether the current config consists of only the defaults.""" 194 | dump_args = dict( 195 | exclude_defaults=True, exclude_none=True, mode="json-compliant" 196 | ) 197 | return type(self)().model_dump(**dump_args) == self.model_dump(**dump_args) 198 | 199 | # project-specific 200 | 201 | project_name: str = Field( 202 | None, 203 | description="Name of the software project (written as it should show up in e.g. documentation).", 204 | exclude=True, 205 | ) 206 | project_description: str = Field( 207 | None, 208 | description="One-line description of the project (<= 512 characters).", 209 | max_length=512, 210 | exclude=True, 211 | ) # NOTE: longer description can cause problems with package building 212 | project_keywords: str = Field( 213 | None, 214 | description="Search keywords characterizing your project (separated by spaces).", 215 | exclude=True, 216 | ) 217 | project_version: SemVerStr = Field( 218 | "0.1.0", 219 | description="[link=https://semver.org]Semantic version number[/link] of the project (i.e., the version to be assigned to the next release).", 220 | exclude=True, 221 | ) 222 | project_year: int = Field( 223 | TODAY.year, 224 | description="Year when the project was initiated (for copyright notice, usually the current year for new repositories).", 225 | exclude=True, 226 | ) 227 | project_license: SPDXLicense = Field( 228 | "MIT", 229 | description="License used for this project (must be a valid [link=https://spdx.org/licenses]SPDX license identifier[/link], such as [b]MIT[/b] or [b]GPL-3.0-only[/b]).", 230 | ) 231 | 232 | # person-specific 233 | 234 | last_name: str = Field( 235 | None, description="Your last name (usually the family name)." 236 | ) 237 | first_name: str = Field( 238 | None, 239 | description="Your first name(s) (and everything else before your last name).", 240 | ) 241 | email: Email = Field( 242 | None, description="Your contact e-mail address for this project." 243 | ) 244 | orcid: Optional[Union[OrcidUrl, Orcid]] = Field( 245 | None, 246 | description="Your [link=https://www.orcid.org]ORCID[/link], as a URL or the raw identifier [b]XXXX-XXXX-XXXX-XXXX[/b] (leave empty if you do not have one yet).", 247 | ) 248 | affiliation: str = Field( 249 | None, description="Your affiliation (usually your employer or a department)." 250 | ) 251 | copyright_holder: str = Field( 252 | None, 253 | description="The copyright holder of your work (usually your employer or a department).", 254 | ) 255 | 256 | # technical 257 | 258 | # NOTE: the URL contains lots of useful information 259 | project_repo_url: Optional[HttpUrl] = Field( 260 | None, 261 | description="URL of the target remote repository at your git hosting service (leave empty if you did not create it yet).", 262 | exclude=True, 263 | ) 264 | 265 | project_hoster: MyHttpUrl = Field( 266 | None, 267 | description="Domain of the hosting service used for the repository (e.g. [b]github.com[/b], [b]gitlab.com[/b] or other GitLab instance).", 268 | ) 269 | project_org: str = Field( 270 | None, 271 | description="GitHub Organization, GitLab Group or Git[Hub|Lab] Username (where the remote repository is/will be located).", 272 | ) 273 | project_slug: str = Field( 274 | None, 275 | description="Machine-friendly name of the project (used as technical package name, for directories, URLs, etc.).", 276 | ) 277 | 278 | project_pages_domain: str = Field( 279 | None, 280 | description="Domain where the GitHub/GitLab Pages are served (e.g. github.com -> [b]github.io[/b], gitlab.com -> [b]gitlab.io[/b], helmholtz.cloud -> [b]pages.hzdr.de[/b])", 281 | ) 282 | project_pages_url: MyHttpUrl = Field( 283 | None, 284 | description="URL where the project Git[Hub|Lab] Pages will be served (if you don't know yet, enter some fake URL and change it later).", 285 | exclude=True, 286 | ) 287 | 288 | init_cli: bool = Field( 289 | False, description="Do you want to add example code for a CLI application?" 290 | ) 291 | init_api: bool = Field( 292 | False, description="Do you want to add example code for a web API service?" 293 | ) 294 | 295 | # derivative values (that are not be overridable by the user) 296 | 297 | def project_package(self) -> str: 298 | """Return valid Python project package name based on project slug.""" 299 | return self.project_slug.replace("-", "_") 300 | 301 | def project_git_path(self) -> str: 302 | return f"{self.project_org}/{self.project_slug}" 303 | 304 | def project_clone_url(self) -> str: 305 | return f"git@{self.project_hoster.host}/{self.project_git_path()}.git" 306 | 307 | def name_email_str(self) -> str: 308 | """Return author string in 'firstName lastName ' format.""" 309 | return f"{self.first_name} {self.last_name} <{self.email}>" 310 | 311 | def copyright_text(self) -> str: 312 | return f"Copyright © {self.project_year} {self.copyright_holder}" 313 | 314 | def copyright_line(self) -> str: 315 | """Return copyright line based on year and copyright holder.""" 316 | return f"SPDX-FileCopyrightText: {self.copyright_text()}" 317 | 318 | # helper functions 319 | 320 | def is_github(self) -> bool: 321 | """Return whether this is GitHub (if it is not, we assume it is a GitLab).""" 322 | return self.project_hoster.host == "github.com" 323 | 324 | def infer_from_output_dir(self, path: Path): 325 | self.project_name = path.name 326 | self.project_slug = pyslugify(path.name) 327 | 328 | def infer_from_repo_url(self, url: Union[str, HttpUrl]): 329 | """Infer field values from passed repository URL.""" 330 | self.project_repo_url = url 331 | repo_url: HttpUrl = self.project_repo_url 332 | 333 | if not repo_url.path: 334 | return # URL does not look right 335 | base = repo_url.path[1:].split("/") 336 | if not len(base) > 1: 337 | return # URL does not look right 338 | slug = base.pop() 339 | 340 | self.project_hoster = repo_url.host 341 | self.project_org = "/".join(base) 342 | self.project_slug = slug 343 | self.project_name = slug 344 | 345 | if pages_domain := PAGES_DOMAINS.get(self.project_hoster.host): 346 | self.project_pages_domain = pages_domain 347 | 348 | if self.project_pages_domain: 349 | # if the service was successfully identified, we can also prefill the project docs URL \_^v^_/ 350 | main_group, subgroups = base[0].lower(), "/".join(base[1:]).lower() 351 | rest_path = "" if not subgroups else f"/{subgroups}" 352 | self.project_pages_url = f"https://{main_group}.{self.project_pages_domain}{rest_path}/{self.project_slug}" 353 | 354 | def infer_repo_url(self): 355 | """Infer and set repo URL from other fields.""" 356 | url = ( 357 | f"https://{self.project_hoster.host}/{self.project_org}/{self.project_slug}" 358 | ) 359 | self.project_repo_url = url 360 | 361 | 362 | class CookiecutterConfig(BaseModel): 363 | """Wrapper class to load cookiecutter configuration file. 364 | 365 | We use a custom section inside it for our pre-set variables, 366 | to avoid polluting the regular namespace or using some special prefix. 367 | """ 368 | 369 | model_config = ConfigDict(extra="allow") 370 | # ---- 371 | 372 | default_context: Optional[Dict[str, Any]] = {} 373 | fair_python_cookiecutter: FPCConfig = Field(default_factory=lambda: FPCConfig()) 374 | 375 | @classmethod 376 | def config_path(cls) -> Path: 377 | """Location of the .cookiecutterrc on the current platform.""" 378 | return Path("~/.cookiecutterrc").expanduser() 379 | 380 | @classmethod 381 | def load(cls, *, config_file: Path = None, default_config: bool = False) -> Self: 382 | """Load config from ~/.cookiecutterrc (if missing, returns defaults).""" 383 | try: 384 | dct = get_user_config( 385 | config_file=config_file, default_config=default_config 386 | ) 387 | except AttributeError: 388 | # workaround for https://github.com/cookiecutter/cookiecutter/issues/1994 389 | # FIXME: once it is fixed in cookiecutter, update deps + remove workaround 390 | dct = get_user_config(config_file=config_file, default_config=True) 391 | 392 | return CookiecutterConfig.model_validate(dct) 393 | 394 | def save(self): 395 | """Save current config to ~/.cookiecutterrc.""" 396 | to_yaml_file(self.config_path(), self, exclude_none=True) 397 | 398 | 399 | class CookiecutterJson(BaseModel): 400 | """Model for cookiecutter.json (compiled down from the config, not type-safe). 401 | 402 | It contains basic and derivative fields based on user inputs that are cumbersome to construct 403 | via Jinja templates or ask the user (cookiecutter private fields are stretched to their limits here). 404 | """ 405 | 406 | model_config = ConfigDict(extra="ignore") 407 | # ---- 408 | # internal 409 | copy_without_render: List[str] = Field( 410 | ["docs/overrides", ".github"], alias="_copy_without_render" 411 | ) 412 | fpc_version: str = Field(__version__, alias="_fpc_version") 413 | 414 | # general 415 | project_name: str 416 | project_slug: str 417 | project_package: str 418 | 419 | project_version: str 420 | project_description: str 421 | project_keywords: str 422 | 423 | # for correct URLs 424 | project_git_hoster: str 425 | project_git_org: str 426 | project_git_path: str 427 | project_repo_url: str 428 | project_clone_url: str 429 | 430 | # for GitHub/Lab Pages 431 | project_pages_domain: str 432 | project_pages_url: str 433 | 434 | # legal 435 | project_license: str 436 | 437 | copyright_holder: str 438 | copyright_year: str 439 | copyright_text: str 440 | copyright_line: str 441 | 442 | # author info 443 | author_affiliation: str 444 | author_last_name: str 445 | author_first_name: str 446 | author_email: str 447 | author_orcid_url: str 448 | author_name_email: str 449 | 450 | # additional settings 451 | init_cli: bool 452 | init_api: bool 453 | is_github: bool 454 | 455 | @classmethod 456 | def from_config(cls, conf: CookiecutterConfig) -> Self: 457 | pconf = conf.fair_python_cookiecutter 458 | ctx = dict(conf.default_context) 459 | ctx.update( 460 | dict( 461 | project_name=pconf.project_name, 462 | project_slug=pconf.project_slug, 463 | project_package=pconf.project_package(), 464 | project_version=pconf.project_version, 465 | project_description=pconf.project_description, 466 | project_keywords=pconf.project_keywords, 467 | project_license=pconf.project_license, 468 | copyright_holder=pconf.copyright_holder, 469 | copyright_year=str(pconf.project_year), 470 | copyright_text=pconf.copyright_text(), 471 | copyright_line=pconf.copyright_line(), 472 | author_affiliation=pconf.affiliation, 473 | author_last_name=pconf.last_name, 474 | author_first_name=pconf.first_name, 475 | author_email=pconf.email, 476 | author_orcid_url=pconf.orcid if pconf.orcid else "", 477 | author_name_email=pconf.name_email_str(), 478 | project_git_hoster=str(pconf.project_hoster), 479 | project_git_org=pconf.project_org, 480 | project_git_path=pconf.project_git_path(), 481 | project_clone_url=pconf.project_clone_url(), 482 | project_repo_url=str(pconf.project_repo_url), 483 | project_pages_domain=pconf.project_pages_domain, 484 | project_pages_url=str(pconf.project_pages_url), 485 | init_cli=pconf.init_cli, 486 | init_api=pconf.init_api, 487 | is_github=pconf.is_github(), 488 | ) 489 | ) 490 | return CookiecutterJson(**ctx) 491 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/main.py: -------------------------------------------------------------------------------- 1 | """Main functions for controlling the template creation.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from shutil import which 6 | from typing import Any, Dict 7 | 8 | from cookiecutter.main import cookiecutter 9 | 10 | from .config import CookiecutterConfig, CookiecutterJson 11 | from .utils import TempDir, copy_template, run_cmd 12 | 13 | 14 | def check_prerequisites(): 15 | needed = ["git", "python", "pip", "poetry"] 16 | for prg in needed: 17 | if not which(prg): 18 | print(f"Program '{prg}' was not found in your environment, cannot proceed!") 19 | exit(1) 20 | 21 | 22 | def strip_yaml_header(file_contents: str): 23 | """Given text file contents, remove YAML header bounded by '---' lines.""" 24 | content = file_contents.splitlines() 25 | startidx = 1 26 | if content and content[0] == "---": 27 | startidx += 1 28 | while not content[startidx - 1].startswith("---"): 29 | startidx += 1 30 | return "\n".join(content[startidx:]) 31 | 32 | 33 | def create_gl_issue_template_from_gh(proj_root: Path): 34 | """Given project path, create GitLab issue templates from GitHub ones.""" 35 | gh_templates = proj_root / ".github" / "ISSUE_TEMPLATE" 36 | gl_templates = proj_root / ".gitlab" / "issue_templates" 37 | 38 | gl_templates.mkdir(parents=True, exist_ok=True) 39 | for file in gh_templates.glob("*"): 40 | with open(gl_templates / file.name, "w") as f: 41 | f.write(strip_yaml_header(open(file).read())) 42 | 43 | 44 | def remove_unneeded_code(proj_root: Path, conf: CookiecutterConfig): 45 | """Remove code examples the user did not wish to have in the project.""" 46 | pkg = conf.fair_python_cookiecutter.project_package() 47 | to_remove = [] 48 | if not conf.fair_python_cookiecutter.init_cli: 49 | to_remove += [ 50 | (proj_root / "src" / pkg / "cli.py"), 51 | (proj_root / "tests" / "test_cli.py"), 52 | ] 53 | if not conf.fair_python_cookiecutter.init_api: 54 | to_remove += [ 55 | (proj_root / "src" / pkg / "api.py"), 56 | (proj_root / "tests" / "test_api.py"), 57 | ] 58 | for file in to_remove: 59 | if file.is_file(): 60 | file.unlink() 61 | 62 | 63 | def download_licenses( 64 | proj_root: Path, conf: CookiecutterConfig, *, force_download: bool = False 65 | ): 66 | """Download all needed licenses and create main LICENSE file.""" 67 | # download licenses if no licenses dir is in the project dir 68 | if force_download or not (proj_root / "LICENSES").is_dir(): 69 | # first rename temp-REUSE.toml to REUSE.toml 70 | if (proj_root / "temp-REUSE.toml").is_file(): 71 | (proj_root / "temp-REUSE.toml").rename(proj_root / "REUSE.toml") 72 | # only install reuse/pipx if it is not found 73 | reuse_cmd = "reuse --suppress-deprecation download --all" 74 | if not which("reuse"): 75 | reuse_cmd = "pipx run " + reuse_cmd 76 | if not which("pipx"): 77 | run_cmd("poetry run pip install pipx", cwd=proj_root) 78 | reuse_cmd = "poetry run " + reuse_cmd 79 | # download licenses 80 | run_cmd(reuse_cmd, cwd=proj_root) 81 | 82 | # copy main license over from resulting licenses directory 83 | license_name = conf.fair_python_cookiecutter.project_license 84 | license = Path(proj_root) / "LICENSE" 85 | license.write_text((proj_root / "LICENSES" / f"{license_name}.txt").read_text()) 86 | 87 | 88 | def init_git_repo(tmp_root: Path, proj_root: Path): 89 | # NOTE: script is OS-agnostic, .bat extension is for windows, bash does not care 90 | post_gen_script = tmp_root / "post_gen_project.bat" 91 | # rewrite newlines correctly for the OS 92 | post_gen_script.write_text(post_gen_script.read_text(), encoding="utf-8") 93 | if os.name != "nt": 94 | run_cmd(f"bash {post_gen_script}", cwd=proj_root) 95 | else: 96 | run_cmd(f"{post_gen_script}", cwd=proj_root) 97 | 98 | 99 | def finalize_repository(tmp_root: Path, proj_root: Path, conf: CookiecutterConfig): 100 | """Finalize instantiated repository based on configuration.""" 101 | create_gl_issue_template_from_gh(proj_root) 102 | remove_unneeded_code(proj_root, conf) 103 | download_licenses(proj_root, conf) 104 | init_git_repo(tmp_root, proj_root) 105 | 106 | 107 | def create_repository( 108 | conf: CookiecutterConfig, 109 | output_dir: Path, 110 | *, 111 | cc_args: Dict[str, Any] = None, 112 | keep_on_fail: bool = False, 113 | ) -> Path: 114 | """Create a new repository based on given configuration, returns resulting directory.""" 115 | cc_json = CookiecutterJson.from_config(conf) 116 | cc_args = cc_args or {} 117 | 118 | check_prerequisites() 119 | with TempDir(keep=keep_on_fail) as tmp_root: 120 | copy_template(tmp_root, cookiecutter_json=cc_json) 121 | cookiecutter( 122 | template=str(tmp_root), # copy of template 123 | no_input=True, # if cookiecutter still needs to ask, we did a mistake 124 | output_dir=str(output_dir), 125 | accept_hooks=True, 126 | keep_project_on_failure=keep_on_fail, 127 | **cc_args, 128 | ) 129 | repo_dir = output_dir / cc_json.project_slug 130 | finalize_repository(tmp_root, repo_dir, conf) 131 | 132 | return repo_dir 133 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "FAIR Python App", 3 | "project_slug": "{{ cookiecutter.project_name | slugify() }}", 4 | "project_package": "{{ cookiecutter.project_name | slugify(separator='_') }}", 5 | 6 | "project_git_hoster": "https://github.com", 7 | "project_git_org": "Github-Organization-Or-Username", 8 | "project_git_path": "{{ cookiecutter_git_org }}/{{ cookiecutter.project_slug }}", 9 | "project_repo_url": "{{ cookiecutter.project_git_hoster }}/{{ cookiecutter.project_git_org }}/{{ cookiecutter.project_slug }}", 10 | "project_clone_url": "git@{{ cookiecutter.project_git_hoster }}:{{ cookiecutter.project_git_path }}.git", 11 | "project_pages_domain": "github.io", 12 | "project_pages_url": "https://{{ cookiecutter.project_git_org }}.{{ cookiecutter.project_pages_url }}/{{ cookiecutter.__project_slug }}", 13 | 14 | "project_version": "0.1.0", 15 | "project_description": "TODO - A meaningful one-sentence description.", 16 | "project_keywords": "TODO list keywords separated by spaces", 17 | "project_license": "mit", 18 | 19 | "author_last_name": "Doe", 20 | "author_first_name": "Jane", 21 | "author_email": "{{ cookiecutter.author_first_name[0].lower() }}.{{ cookiecutter.author_last_name.lower() }}@my-organization.org", 22 | "author_orcid": "", 23 | "author_affiliation": "{{ cookiecutter.__org_name }}", 24 | 25 | "author_name_email": "{{ cookiecutter.author_first_name }} {{ cookiecutter.author_last_name }} <{{ cookiecutter.author_email }}>", 26 | "author_orcid_url": "https://orcid.org/{{ cookiecutter.author_orcid }}", 27 | 28 | "copyright_year": "{% now 'local', '%Y' %}", 29 | "copyright_holder": "{{ cookiecutter.author_affiliation }}", 30 | "copyright_text": "{{ cookiecutter.copyright_year }} {{ cookiecutter.copyright_holder }}", 31 | "copyright_line": "SPDX-FileCopyrightText: © {{ cookiecutter.copyright_text }}", 32 | 33 | "init_cli": false, 34 | "init_api": false, 35 | "is_github": true, 36 | 37 | "_copy_without_render": ["docs/overrides", ".github"] 38 | } 39 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/post_gen_project.bat: -------------------------------------------------------------------------------- 1 | : # Magic to deactivate current Python venv (if one is enabled) in a cross-platform way 2 | : # See https://stackoverflow.com/questions/17510688/single-script-to-run-in-both-windows-batch-and-linux-bash 3 | :<<"::CMDLITERAL" 4 | : ---- code for cmd.exe ---- 5 | for /f "delims=" %%a in ('python -c "import sys; print(sys.prefix if sys.base_prefix != sys.prefix else '')"') do set "VENV_PATH=%%a" 6 | IF NOT "%VENV_PATH%" == "" ( 7 | echo INFO: Deactivating currently active virtual environment "%VENV_PATH%" 8 | REM Assuming the virtual environment needs to be activated first to provide the deactivate script 9 | call "%VENV_PATH%\Scripts\activate.bat" 10 | call "%VENV_PATH%\Scripts\deactivate.bat" 11 | ) 12 | : ---------------------------- 13 | GOTO :COMMON 14 | ::CMDLITERAL 15 | # ---- bash-specific code ---- 16 | venv=$(python -c "import sys; print(sys.prefix if sys.base_prefix != sys.prefix else '')") 17 | if [[ -n "$venv" ]]; then 18 | echo INFO: Deactivating currently active virtual environment "$venv" 19 | source "$venv/bin/activate" # make sure we have 'deactivate' available 20 | deactivate 21 | fi 22 | # ---------------------------- 23 | :<<"::CMDLITERAL" 24 | :COMMON 25 | ::CMDLITERAL 26 | 27 | : #All following code must be hybrid (work for bash and cmd.exe) 28 | : # ------------------------------------------------------------ 29 | 30 | echo "Initializing the git repository ..." 31 | 32 | git init 33 | poetry install --with docs 34 | poetry run poe init-dev 35 | 36 | echo "Creating CITATION.cff and codemeta.json using somesy ..." 37 | 38 | git add . 39 | poetry run pre-commit run somesy 40 | git add . 41 | 42 | echo "Running all other hooks ..." 43 | 44 | poetry run pre-commit run --all 45 | git add . 46 | 47 | echo "Creating first commit ..." 48 | 49 | poetry run git commit -m "generated project using fair-python-cookiecutter" -m "https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter" 50 | 51 | echo "Ensuring that the default branch is called 'main' ..." 52 | 53 | git branch -M main 54 | 55 | echo "--------> All done! Your project repository is ready :) <--------" 56 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Traces** 26 | If applicable, provide a Python stack trace or JavaScript console log from the browser 27 | 28 | **Environment** 29 | Provide information about versions of relevant software packages. 30 | 31 | - Python Version (e.g. 3.9.10) 32 | - poetry version (e.g. 1.2.1) 33 | - Browser [e.g. chrome, safari] + Version 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | # Main CI pipeline of the repository. 3 | # 4 | # Overview: 5 | # Lint --> test doc build -\ 6 | # \-> test code ---> deploy docs (*) -> release (**) 7 | # 8 | # (*): only on push of primary branches + release tags 9 | # (**): only for release version tags (vX.Y.Z) 10 | 11 | on: 12 | push: 13 | branches: [main, dev] 14 | tags: ["v*.*.*"] 15 | pull_request: 16 | types: [opened, reopened, synchronize, ready_for_review] 17 | 18 | jobs: 19 | 20 | lint: 21 | # run general checks that do not require installing the package 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Install poetry 27 | run: pipx install poetry 28 | - uses: actions/setup-python@v5 29 | with: 30 | python-version: "3.10" 31 | 32 | - name: Install poe, pre-commit and safety 33 | run: pip install poethepoet pre-commit safety 34 | 35 | # NOTE: using custom cache, to include pre-commit linters + deps 36 | - uses: actions/cache@v4 37 | with: 38 | path: | 39 | ~/.cache/pre-commit 40 | ~/.cache/pip 41 | key: ${{ hashFiles('.pre-commit-config.yaml') }}-pre-commit 42 | 43 | - name: Check that all static analysis tools run without errors 44 | run: poetry run poe lint --all-files 45 | 46 | - name: Scan dependencies for known vulnerabilities 47 | run: safety check -r pyproject.toml 48 | 49 | test-build-docs: 50 | # make sure that documentation is buildable 51 | # (better to know that before e.g. a PR is merged) 52 | needs: lint 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Install poetry 58 | run: pipx install poetry 59 | - uses: actions/setup-python@v5 60 | with: 61 | python-version: "3.10" 62 | cache: "poetry" 63 | 64 | - name: Check that documentation builds without errors 65 | run: | 66 | poetry install --with docs 67 | poetry run poe docs --verbose 68 | 69 | test: 70 | # run tests with different OS and Python combinations 71 | needs: lint 72 | strategy: 73 | fail-fast: true 74 | matrix: 75 | os: [ "ubuntu-latest", "macos-latest", "windows-latest" ] 76 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] 77 | runs-on: ${{ matrix.os }} 78 | 79 | steps: 80 | - uses: actions/checkout@v4 81 | - name: Install poetry 82 | run: pipx install poetry 83 | - uses: actions/setup-python@v5 84 | with: 85 | python-version: ${{ matrix.python-version }} 86 | cache: "poetry" 87 | 88 | - name: Check that tests complete without errors 89 | run: | 90 | poetry install 91 | poetry run poe test 92 | 93 | docs: 94 | # build + deploy documentation (only on push event for certain branches+tags) 95 | needs: [test, test-build-docs] 96 | if: github.event_name == 'push' 97 | runs-on: ubuntu-latest 98 | permissions: 99 | contents: write 100 | 101 | steps: 102 | - uses: actions/checkout@v4 103 | - name: Install poetry 104 | run: pipx install poetry 105 | - uses: actions/setup-python@v5 106 | with: 107 | python-version: "3.10" 108 | cache: "poetry" 109 | 110 | - name: Install project with mkdocs and plugins 111 | run: poetry install --with docs 112 | 113 | - name: Configure Git user (Github Actions Bot) 114 | run: | 115 | git config --local user.name "github-actions[bot]" 116 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 117 | 118 | - name: Check out or initialize gh-pages branch 119 | run: | 120 | if git fetch origin gh-pages:gh-pages 121 | then 122 | echo "Found existing gh-pages branch." 123 | else 124 | echo "Creating new gh-pages branch and initializing mike." 125 | poetry run mike deploy -u ${{ github.ref_name }} latest 126 | poetry run mike set-default latest 127 | fi 128 | 129 | - name: Build and deploy documentation to gh-pages 130 | run: | 131 | SET_LATEST="" 132 | if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+*$ ]]; then 133 | # if a new release tag is pushed, mark the built documentation as 'latest' 134 | SET_LATEST="latest" 135 | fi 136 | poetry run mike deploy -u --push ${{ github.ref_name }} $SET_LATEST 137 | 138 | publish: 139 | # if a version tag is pushed + tests + docs completed -> do release 140 | needs: docs 141 | if: startswith(github.ref, 'refs/tags/v') 142 | permissions: 143 | contents: write # for GitHub release 144 | id-token: write # for PyPI release 145 | 146 | uses: "./.github/workflows/release.yml" 147 | with: 148 | to_github: true 149 | to_test_pypi: false 150 | to_pypi: false 151 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | # Release a new version to different targets in suitable ways 3 | 4 | on: 5 | workflow_call: # called from ci.yml 6 | inputs: 7 | to_github: 8 | description: "Create a Github Release (repository snapshot)" 9 | type: boolean 10 | default: true 11 | 12 | to_test_pypi: 13 | description: "Publish to Test PyPI." 14 | type: boolean 15 | default: false 16 | 17 | to_pypi: 18 | description: "Publish to PyPI." 19 | type: boolean 20 | default: false 21 | 22 | 23 | jobs: 24 | 25 | github: 26 | if: inputs.to_github 27 | name: Create a Github Release (repository snapshot) 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write # needed for creating a GH Release 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: softprops/action-gh-release@v1 34 | 35 | pypi: 36 | if: inputs.to_pypi || inputs.to_test_pypi 37 | name: Publish to PyPI (and/or compatible repositories) 38 | runs-on: ubuntu-latest 39 | permissions: 40 | id-token: write # needed for "trusted publishing" protocol 41 | steps: 42 | - uses: actions/checkout@v3 43 | 44 | - name: Install poetry 45 | run: pipx install poetry 46 | 47 | - uses: actions/setup-python@v4 48 | with: 49 | python-version: "3.10" 50 | cache: "poetry" 51 | 52 | - name: Build the distribution package 53 | run: poetry build 54 | 55 | - name: Publish package to TestPyPI 56 | if: inputs.to_test_pypi 57 | uses: pypa/gh-action-pypi-publish@release/v1 58 | with: 59 | repository-url: https://test.pypi.org/legacy/ 60 | 61 | - name: Publish package to PyPI 62 | if: inputs.to_pypi 63 | uses: pypa/gh-action-pypi-publish@release/v1 64 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.gitignore: -------------------------------------------------------------------------------- 1 | # don't add custom python shell scratchpad file 2 | scratch*.py 3 | 4 | # generated badges 5 | docs/*_badge.svg 6 | 7 | # don't add vscode stuff 8 | .vscode 9 | 10 | # Created by https://www.toptal.com/developers/gitignore/api/python 11 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 12 | 13 | ### Python ### 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | pytestdebug.log 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | doc/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # poetry 108 | #poetry.lock 109 | 110 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 111 | __pypackages__/ 112 | 113 | # Celery stuff 114 | celerybeat-schedule 115 | celerybeat.pid 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .env/ 123 | .venv/ 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | pythonenv* 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # operating system-related files 153 | # file properties cache/storage on macOS 154 | *.DS_Store 155 | # thumbnail cache on Windows 156 | Thumbs.db 157 | 158 | # profiling data 159 | .prof 160 | 161 | 162 | # End of https://www.toptal.com/developers/gitignore/api/python 163 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # Used CI pipeline stages (see https://docs.gitlab.com/ee/ci/yaml/#stages) 2 | stages: 3 | - check # runs all linters 4 | - test # runs pytest tests with different Python versions 5 | - docs # builds and deploys documentation to GitLab Pages 6 | - release # deploy release to GitLab/PyPI/Test-PyPI (for pushed release tags only) 7 | 8 | # Set some environment variables based on Gitlab-specific variables 9 | # See https://docs.gitlab.com/ee/ci/variables/ 10 | variables: 11 | # documentation and package release configuration 12 | # (NOTE: adjust as needed for your project, depending on your environment) 13 | # ---- 14 | release_docs_pages: "true" # NOTE: needs CI variable PAGES_TOKEN 15 | release_to_gitlab: "true" 16 | release_to_testpypi: "false" # NOTE: needs CI variable RELEASE_TOKEN_testpypi 17 | release_to_pypi: "false" # NOTE: needs CI variable RELEASE_TOKEN_pypi 18 | release_to_custom: "false" # NOTE: needs CI variable RELEASE_TOKEN_custom 19 | # ---- 20 | # 21 | # settings for caching 22 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" 23 | PRE_COMMIT_HOME: "$CI_PROJECT_DIR/.cache/pre-commit" 24 | 25 | # Global cache settings for test jobs (each job by default uses own copy of these settings) 26 | # If key files do not change, restore the listed paths from cache (if cached). 27 | # At the end, store those directories in cache. 28 | # See https://docs.gitlab.com/ee/ci/caching/ 29 | cache: &global_cache 30 | key: 31 | files: 32 | - poetry.lock 33 | - .pre-commit-config.yaml 34 | prefix: $CI_JOB_NAME 35 | paths: 36 | - .cache 37 | - .venv 38 | policy: pull-push 39 | 40 | # Prepare environment with all required tools. 41 | # Defined as hidden job (starting with dot) + YAML anchor (for referencing) 42 | # See https://docs.gitlab.com/ee/ci/yaml/#anchors 43 | .prepare-env-template: &prepare-env 44 | before_script: 45 | # install needed tools 46 | - pip install pipx==1.2.0 47 | - pipx ensurepath 48 | - pipx install poetry 49 | - export PATH="${PATH}:~/.local/bin" # put poetry on PATH 50 | - ln -s ~/.local/bin/poetry /usr/bin/poetry # for poe (PATH is set only locally) 51 | - poetry --version 52 | - poetry config virtualenvs.in-project true 53 | # install deps and activate venv 54 | - poetry install --no-root 55 | - source .venv/bin/activate 56 | 57 | # ---- 58 | 59 | # Run pre-commit, just to make sure the developers did not mess up and forgot it. 60 | # It uses its own cache for its isolated environments (location: PRE_COMMIT_HOME). 61 | run-pre-commit: 62 | image: python:latest 63 | stage: check 64 | <<: *prepare-env 65 | script: 66 | - poetry run poe lint --all-files 67 | - pipx install safety 68 | - safety check -r pyproject.toml 69 | 70 | # ---- 71 | 72 | # test building docs 73 | test-docs-build: 74 | image: python:latest 75 | stage: test 76 | variables: 77 | <<: *prepare-env 78 | script: 79 | - poetry install --with docs 80 | - poetry run poe docs --verbose 81 | 82 | # pytest job template (combine with image(s) containing a specific Python version): 83 | # 84 | # NOTE: it is not possible to easily test against a specific operation system, 85 | # using GitLab CI, as this depends on the used runner (which is usually Linux-based). 86 | .run-pytest-template: &run-pytest 87 | stage: test 88 | <<: *prepare-env 89 | script: 90 | - poetry install 91 | - poetry run poe test --cov --junitxml=report.xml 92 | artifacts: 93 | when: always 94 | reports: 95 | junit: report.xml # for Gitlab integration of test results 96 | 97 | # Run tests, using different Python versions: 98 | run-pytest-3.9: 99 | image: python:3.9 100 | <<: *run-pytest 101 | run-pytest-3.10: 102 | image: python:3.10 103 | <<: *run-pytest 104 | run-pytest-3.11: 105 | image: python:3.11 106 | <<: *run-pytest 107 | run-pytest-3.12: 108 | image: python:3.12 109 | <<: *run-pytest 110 | run-pytest-3.13: 111 | image: python:3.13 112 | <<: *run-pytest 113 | 114 | # ---- 115 | 116 | # Update or push documentation for the current branch or a new tagged release 117 | # See https://github.com/jimporter/mike/issues/25 118 | pages: 119 | image: python:latest 120 | stage: docs 121 | rules: 122 | - if: "($release_docs_pages == 'true' || $release_docs_pages == '1') && $CI_PIPELINE_SOURCE != 'merge_request_event'" 123 | variables: 124 | PAGES_BRANCH: gl-pages 125 | HTTPS_REMOTE: https://gitlab-ci-token:${PAGES_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git 126 | <<: *prepare-env 127 | script: 128 | # Preparation: we need to clone the repository in the job and directly access it via git 129 | - poetry install --with docs 130 | - git config user.name $GITLAB_USER_NAME 131 | - git config user.email $GITLAB_USER_EMAIL 132 | - git fetch origin $PAGES_BRANCH && git -b checkout $PAGES_BRANCH origin/$PAGES_BRANCH || git checkout $PAGES_BRANCH || echo "Pages branch not deployed yet." 133 | - git checkout $CI_COMMIT_SHA 134 | # If triggered by a push, check if the branch is enabled for pages 135 | - | 136 | if [[ -n "$CI_COMMIT_BRANCH" ]] && ! [[ "$CI_COMMIT_BRANCH" =~ ^(main|dev)$ ]]; then 137 | echo "INFO: Branch not enabled for pages, skipping docs deployment ('$PAGES_BRANCH' branch unchanged)." 138 | git checkout $PAGES_BRANCH -- public/ 139 | exit 0 140 | fi 141 | # Deploy pages for a tag or enabled branch 142 | - | 143 | if [[ -z "$PAGES_TOKEN" ]]; then 144 | echo "ERROR: CI variable PAGES_TOKEN is missing, cannot proceed!" 145 | exit 1 146 | fi 147 | - DOCS_TAG_RAW=${CI_COMMIT_TAG:=$CI_COMMIT_BRANCH} # get tag (or if not present, current branch) 148 | - DOCS_TAG=${DOCS_TAG_RAW//\//_} # eliminate '/' symbols from branch (or tag) name 149 | - echo Deploying docs for "$DOCS_TAG" 150 | - mike deploy --deploy-prefix public -r $HTTPS_REMOTE -p -b $PAGES_BRANCH -u $DOCS_TAG latest 151 | - mike set-default --deploy-prefix public -r $HTTPS_REMOTE -p -b $PAGES_BRANCH latest 152 | - git checkout $PAGES_BRANCH -- public/ 153 | artifacts: 154 | paths: 155 | - public/ 156 | 157 | # ---- 158 | 159 | # Create a new GitLab release if a tag is pushed (only if release_to_gitlab is true) 160 | # https://docs.gitlab.com/ee/user/project/releases/release_cicd_examples.html 161 | release_gitlab: 162 | stage: release 163 | image: registry.gitlab.com/gitlab-org/release-cli:latest 164 | rules: 165 | - if: "$CI_COMMIT_TAG && ($release_to_gitlab == 'true' || $release_to_gitlab == '1')" 166 | script: 167 | - echo "creating GitLab Release for tag '$CI_COMMIT_TAG'" 168 | release: 169 | tag_name: '$CI_COMMIT_TAG' 170 | description: '$CI_COMMIT_TAG' 171 | 172 | # Common script to publish to PyPI, Test PyPI or other custom instances 173 | .release-pypi-common: &release-pypi-common 174 | stage: release 175 | image: python:latest 176 | <<: *prepare-env 177 | script: 178 | - | # set up custom repository for poetry, if corresponding variables are set 179 | if [[ -n "$PKGIDX_NAME" ]]; then 180 | [[ -n "$PKGIDX_URL" ]] || (echo "ERROR: PKGIDX_NAME is provided, but PKGIDX_URL is missing!" && exit 1) 181 | poetry config "repositories.$PKGIDX_NAME" "$PKGIDX_URL" 182 | fi 183 | # determine correct CI token variables to use 184 | - PKGIDX_NAME=${PKGIDX_NAME:=pypi} 185 | - TOKVAR=RELEASE_TOKEN_${PKGIDX_NAME} 186 | - echo Will use token from CI variable $TOKVAR for publishing to $PKGIDX_NAME 187 | - | # check that the correct token is actually provided 188 | if [[ -z "${!TOKVAR}" ]] ; then 189 | echo "ERROR: CI variable $TOKVAR is missing, cannot proceed!" 190 | echo "Please provide the token for $PKGIDX_NAME as a masked CI variable called $TOKVAR!" 191 | exit 1 192 | fi 193 | # set up credentials, build package and upload it to the desired package index 194 | - poetry config -- http-basic.pypi "__token__" "${!TOKVAR}" 195 | - poetry build 196 | - poetry publish -r $PKGIDX_NAME $POETRY_PUBLISH_EXTRA_ARGS 197 | 198 | # Create a new PyPI release if a tag is pushed (only if release_to_pypi is true) 199 | release_pypi: 200 | rules: 201 | - if: "$CI_COMMIT_TAG && ($release_to_pypi == 'true' || $release_to_pypi == '1')" 202 | <<: *release-pypi-common 203 | 204 | # Create a new Test PyPI release if a tag is pushed (only if release_to_testpypi is true) 205 | release_test_pypi: 206 | rules: 207 | - if: "$CI_COMMIT_TAG && ($release_to_testpypi == 'true' || $release_to_testpypi == '1')" 208 | <<: *release-pypi-common 209 | variables: 210 | PKGIDX_NAME: "testpypi" 211 | PKGIDX_URL: "https://test.pypi.org/legacy/" 212 | 213 | # Create a new custom release if a tag is pushed (only if release_to_custom is true) 214 | # NOTE: adjust the URL to the API of your custom package index in order to use this job 215 | release_custom_pypi: 216 | rules: 217 | - if: "$CI_COMMIT_TAG && ($release_to_custom == 'true' || $release_to_custom == '1')" 218 | <<: *release-pypi-common 219 | variables: 220 | PKGIDX_NAME: "custom" 221 | PKGIDX_URL: "https://your.custom.package-index.org/legacy/" 222 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.gitlab/issue_templates/Default.md: -------------------------------------------------------------------------------- 1 | Please select a suitable issue template! 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | # GH Actions 5 | - repo: https://github.com/python-jsonschema/check-jsonschema 6 | rev: '0.31.2' 7 | hooks: 8 | - id: check-github-workflows 9 | 10 | # Quality 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | rev: v0.9.9 13 | hooks: 14 | - id: ruff 15 | types_or: [python, pyi, jupyter] 16 | args: [--fix] 17 | - id: ruff-format 18 | types_or: [python, pyi, jupyter] 19 | 20 | - repo: https://github.com/pre-commit/mirrors-mypy 21 | rev: 'v1.15.0' 22 | hooks: 23 | - id: mypy 24 | args: [--no-strict-optional, --ignore-missing-imports] 25 | # NOTE: you might need to add some deps here: 26 | additional_dependencies: [] 27 | 28 | # License Metadata 29 | - repo: https://github.com/citation-file-format/cff-converter-python 30 | rev: '054bda51dbe278b3e86f27c890e3f3ac877d616c' 31 | hooks: 32 | - id: validate-cff 33 | - repo: https://github.com/fsfe/reuse-tool 34 | rev: 'v5.0.2' 35 | hooks: 36 | - id: reuse 37 | 38 | # Project Metadata 39 | - repo: https://github.com/Materials-Data-Science-and-Informatics/somesy 40 | rev: 'v0.7.1' 41 | hooks: 42 | - id: somesy 43 | 44 | # Various general + format-specific helpers 45 | # (run last to fix general syntactic defects) 46 | - repo: https://github.com/pre-commit/pre-commit-hooks 47 | rev: v5.0.0 48 | hooks: 49 | # - id: mixed-line-ending 50 | # args: [--fix=lf] 51 | - id: check-symlinks 52 | - id: trailing-whitespace 53 | exclude: 'CITATION.cff' 54 | - id: check-yaml 55 | exclude: 'mkdocs.yml' 56 | - id: check-toml 57 | - id: check-json 58 | - id: check-ast 59 | - id: debug-statements 60 | - id: check-merge-conflict 61 | - id: check-shebang-scripts-are-executable 62 | - id: check-added-large-files 63 | args: [--maxkb=10000] 64 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors and Contributors 2 | 3 | **Main authors** are persons whose contributions significantly shaped 4 | the state of the software at some point in time. 5 | 6 | **Additional contributors** are persons who are not main authors, 7 | but contributed non-trivially to this project, 8 | e.g. by providing smaller fixes and enhancements to the code and/or documentation. 9 | 10 | Of course, this is just a rough overview and categorization. 11 | For a more complete overview of all contributors and contributions, 12 | please inspect the git history of this repository. 13 | 14 | ## Main Authors 15 | 16 | - {{ cookiecutter.author_first_name }} {{ cookiecutter.author_last_name }} ( 17 | [E-Mail](mailto:{{ cookiecutter.author_email }}), 18 | [ORCID]({{ cookiecutter.author_orcid_url }}) 19 | ): original author 20 | 21 | ## Additional Contributors 22 | 23 | 27 | 28 | ... maybe **[you]({{ cookiecutter.project_pages_url }}/main/contributing)**? 29 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Here we provide notes that summarize the most important changes in each released version. 4 | 5 | Please consult the changelog to inform yourself about breaking changes and security issues. 6 | 7 | ## [v{{ cookiecutter.project_version }}]({{ cookiecutter.project_repo_url }}/tree/v{{ cookiecutter.project_version }}) ({% now 'utc', '%Y-%m-%d' %}) { id="{{ cookiecutter.project_version }}" } 8 | 9 | * First release 10 | 11 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | type: software 3 | message: If you use this software, please cite it using this metadata. 4 | 5 | title: "{{ cookiecutter.project_name }}" 6 | version: "{{ cookiecutter.project_version }}" 7 | abstract: "{{ cookiecutter.project_description }}" 8 | repository-code: "{{ cookiecutter.project_repo_url }}" 9 | license: "{{ cookiecutter.project_license }}" 10 | keywords: {{ cookiecutter.project_keywords.split() | jsonify }} 11 | authors: 12 | - family-names: "{{ cookiecutter.author_last_name }}" 13 | given-names: "{{ cookiecutter.author_first_name }}" 14 | email: "{{ cookiecutter.author_email }}" 15 | {%- if cookiecutter.author_orcid_url %} 16 | orcid: "{{ cookiecutter.author_orcid_url }}" 17 | {%- else %} 18 | # orcid: "https://orcid.org/your-orcid" 19 | {%- endif %} 20 | affiliation: "{{ cookiecutter.author_affiliation }}" 21 | 22 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the project maintainers by e-mail. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 125 | [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 129 | [Mozilla CoC]: https://github.com/mozilla/diversity 130 | [FAQ]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | 133 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | All kinds of contributions are very welcome! 4 | You can contribute in various ways, e.g. by 5 | 6 | - providing feedback 7 | - asking questions 8 | - suggesting ideas 9 | - implementing features 10 | - fixing problems 11 | - improving documentation 12 | 13 | To make contributing to open source projects a good experience to everyone involved, 14 | please make sure that you follow our code of conduct when communicating with others. 15 | 16 | ## Ideas, Questions and Problems 17 | 18 | If you have questions or difficulties using this software, 19 | please use the [issue tracker]({{ cookiecutter.project_repo_url }}/issues). 20 | 21 | If your topic is not already covered by an existing issue, 22 | please create a new issue using one of the provided issue templates. 23 | 24 | If your issue is caused by incomplete, unclear or outdated documentation, 25 | we are also happy to get suggestions on how to improve it. 26 | Outdated or incorrect documentation is a _bug_, 27 | while missing documentation is a _feature request_. 28 | 29 | **NOTE:** If you want to report a critical security problem, _do not_ open an issue! 30 | Instead, please create a [private security advisory](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability), 31 | or contact the current package maintainers directly by e-mail. 32 | 33 | ## Development 34 | 35 | This project uses [Poetry](https://python-poetry.org/) for dependency management. 36 | 37 | You can run the following lines to check out the project and prepare it for development: 38 | 39 | ``` 40 | git clone {{ cookiecutter.project_repo_url }} 41 | cd {{ cookiecutter.project_slug }} 42 | poetry install --with docs 43 | poetry run poe init-dev 44 | ``` 45 | 46 | Common tasks are accessible via [poe](https://github.com/nat-n/poethepoet): 47 | 48 | - Use `poetry run poe lint` to run linters manually, add `--all-files` to check everything. 49 | 50 | - Use `poetry run poe test` to run tests, add `--cov` to also show test coverage. 51 | 52 | - Use `poetry run poe docs` to generate local documentation 53 | 54 | In order to contribute code, please open a pull request. 55 | 56 | Before opening the PR, please make sure that your changes 57 | 58 | - are sufficiently covered by meaningful **tests**, 59 | - are reflected in suitable **documentation** (API docs, guides, etc.), and 60 | - successfully pass all pre-commit hooks. 61 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/README.md: -------------------------------------------------------------------------------- 1 | [ 2 | ![Docs](https://img.shields.io/badge/read-docs-success) 3 | ]({{ cookiecutter.project_pages_url }}) 4 | [ 5 | ![Test Coverage]({{ cookiecutter.project_pages_url }}/main/coverage_badge.svg) 6 | ]({{ cookiecutter.project_pages_url }}/main/coverage) 7 | {%- if cookiecutter.is_github %} 8 | [ 9 | ![CI](https://img.shields.io/github/actions/workflow/status/{{ cookiecutter.project_git_path }}/ci.yml?branch=main&label=ci) 10 | ](https://github.com/{{ cookiecutter.project_git_path }}/actions/workflows/ci.yml) 11 | {% endif %} 12 | 13 | 14 | # {{ cookiecutter.project_name.strip() }} 15 | 16 | ---- 17 | **:warning: TODO: Complete project setup :construction:** 18 | 19 | This is a Python project generated from the 20 | [fair-python-cookiecutter](https://github.com/Materials-Data-Science-and-Informatics/fair-python-cookiecutter) 21 | template. 22 | 23 | **TODO:** To finalize the project setup, please carefully read and follow the instructions in the 24 | [developer guide](https://materials-data-science-and-informatics.github.io/fair-python-cookiecutter/latest/dev_guide). 25 | A copy of the guide is included in your project in `docs/dev_guide.md`. 26 | 27 | ---- 28 | 29 | {{ cookiecutter.project_description.strip() }} 30 | 31 | **:construction: TODO: Write a paragraph summarizing what this project is about.** 32 | 33 | 34 | 35 | 36 | ## Installation 37 | 38 | **TODO: check that the installation instructions work** 39 | 40 | This project works with Python > 3.9. 41 | 42 | ```bash 43 | pip install git+ssh://{{ cookiecutter.project_clone_url }} 44 | ``` 45 | 46 | ## Getting Started 47 | 48 | **TODO: provide a minimal working example** 49 | 50 | 51 | 52 | ## Troubleshooting 53 | 54 | ### When I try installing the package, I get an `IndexError: list index out of range` 55 | 56 | Make sure you have `pip` > 21.2 (see `pip --version`), older versions have a bug causing 57 | this problem. If the installed version is older, you can upgrade it with 58 | `pip install --upgrade pip` and then try again to install the package. 59 | 60 | **You can find more information on using and contributing to this repository in the 61 | [documentation]({{ cookiecutter.project_pages_url }}/main).** 62 | 63 | 64 | 65 | ## How to Cite 66 | 67 | If you want to cite this project in your scientific work, 68 | please use the [citation file](https://citation-file-format.github.io/) 69 | in the [repository]({{ cookiecutter.project_repo_url }}/blob/main/CITATION.cff). 70 | 71 | 72 | 73 | 74 | ## Acknowledgements 75 | 76 | We kindly thank all 77 | [authors and contributors]({{ cookiecutter.project_pages_url }}/latest/credits). 78 | 79 | **TODO: relevant organizational acknowledgements (employers, funders)** 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/changelog.md: -------------------------------------------------------------------------------- 1 | --8<-- "CHANGELOG.md" 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/code_of_conduct.md: -------------------------------------------------------------------------------- 1 | --8<-- "CODE_OF_CONDUCT.md" 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/contributing.md: -------------------------------------------------------------------------------- 1 | --8<-- "CONTRIBUTING.md" 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/credits.md: -------------------------------------------------------------------------------- 1 | --8<-- "AUTHORS.md" 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/css/style.css: -------------------------------------------------------------------------------- 1 | /* you can add your CSS customizations in this file */ 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/dev_guide.md: -------------------------------------------------------------------------------- 1 | # Developer Guide 2 | 3 | This guide is targeting mainly developers, maintainers and other technical contributors 4 | and provides more information on how to work with this repository. 5 | 6 | !!! warning "Important Information" 7 | ## TODO: Final Steps 8 | 9 | Dear project author, thank you for using `fair-python-cookiecutter`! 10 | 11 | Before diving into your actual project work, please complete the following 12 | steps to finalize the configuration of your project repository: 13 | 14 | ### Inspect the generated project files 15 | 16 | We suggest that first you familiarize yourself with the generated structure and make it "your own". 17 | The following sections of this guide provide a high-level overview, but you might want to 18 | inspect the various files to get a better understanding. A few files 19 | contain **TODO** items or sections -- please complete and remove them. 20 | 21 | ### Test the tools locally 22 | 23 | After having some idea about the repository structure, we suggest that you try to run 24 | some operations, such as linting, running tests and building the documentation on your computer. 25 | 26 | ### Push the repository 27 | 28 | If you have not created an empty repository in your git hosting service already, 29 | you should create it now. Follow the instructions of your hosting service to 30 | *push an existing repository* (i.e. this one), which will consist of 31 | 32 | 1. adding the remote repository locally (`git remote add ...`) 33 | 2. pushing the contents to the remote (`git push`) 34 | 35 | ### Check the CI 36 | 37 | Your first push should have automatically triggered the CI pipeline. 38 | Please check that it runs successfully. 39 | 40 | ### Set up Pages and Releases 41 | 42 | For deployment of documentation pages and releases of your code, some additional 43 | configuration is required. Please consult the corresponding sections of this guide. 44 | 45 | ## Overview 46 | 47 | ### Repository Structure 48 | 49 | Here is a *non-exhaustive* list of the most important files and directories in the repository. 50 | 51 | === "General" 52 | - `AUTHORS.md`: acknowledges and lists all contributors 53 | - `CHANGELOG.md`: summarizes the changes for each version of the software for users 54 | - `CODE_OF_CONDUCT.md`: defines the social standards that must be followed by contributors 55 | - `CONTRIBUTING.md`: explains how others can contribute to the project 56 | - `README.md`: provides an overview and points to other resources 57 | 58 | === "Metadata" 59 | - `CITATION.cff`: metadata stating how to cite the project 60 | - `codemeta.json`: metadata for harvesting by other tools and services 61 | - `LICENSE`: the (main) license of the project 62 | - `LICENSES`: copies of all licenses that apply to files in the project 63 | - `REUSE.toml`: granular license and copyright information for all files and directories 64 | 65 | === "Development" 66 | - `pyproject.toml`: project metadata, dependencies, development tool configurations 67 | - `poetry.lock`: needed for reproducible installation of the project 68 | - `src`: actual code provided by the project 69 | - `tests`: all tests for the code in the project 70 | - `mkdocs.yml`: configuration of the project website 71 | - `docs`: most contents used for the project website 72 | 73 | === "CI / QA" 74 | - `.pre-commit-config.yaml`: quality assurance tools used in the project 75 | - `.github/workflows`: CI scripts for GitHub (QA, documentation and package deployment) 76 | - `.github/ISSUE_TEMPLATE`: templates for the GitHub issue tracker 77 | - `.gitlab-ci.yml`: mostly equivalent CI scripts, but for GitLab 78 | - `.gitlab/issue_templates`: The same issues templates, but for GitLab 79 | 80 | !!! tip 81 | You might find various other files popping up which are generated by different tools. 82 | Most of these should not be committed into the repository, so they are excluded 83 | in the `.gitignore` file. Everything listed there is safe to delete. 84 | 85 | ### Used Tools 86 | 87 | Here is a *non-exhaustive* list of the most important tools used in the project. 88 | 89 | === "General" 90 | - `poetry` for dependency management and packaging 91 | - `poethepoet` tool for running common tasks 92 | - `pre-commit` for orchestrating linters, formatters and other utilities 93 | - `mkdocs` for generating the project documentation website 94 | - `mike` for managing the `mkdocs`-generated documentation website 95 | 96 | === "Code Quality" 97 | - `flake8` for general linting (using various linter plugins) 98 | - `mypy` for editor-independent type-checking 99 | - `pytest` for unit testing 100 | - `pytest-cov` for computing code coverage by tests 101 | - `hypothesis` for property-based testing 102 | - `bandit` for checking security issues in the code 103 | - `safety` for checking security issues in the current dependencies 104 | 105 | === "Formatting and Style" 106 | - `black` for source-code formatting 107 | - `autoflake` for automatically removing unused imports 108 | - `pydocstyle` for checking docstring conventions 109 | 110 | === "FAIR metadata" 111 | - `cffconvert` to check the `CITATION.cff` (citation metadata) 112 | - `codemetapy` to generate a `codemeta.json` (general software metadata) 113 | - `somesy` to keep all important metadata continuously synchronized 114 | - `reuse` to check [REUSE-compliance](https://reuse.software/spec/) (granular copyright and license metadata) 115 | - `licensecheck` to scan for possible license incompatibilities in the dependencies 116 | 117 | !!! tip 118 | Most tools installed and used by this project are listed in the 119 | `pyproject.toml` and `.pre-commit-config.yaml` files. 120 | 121 | ## Basics 122 | 123 | The project 124 | 125 | * heavily uses `pyproject.toml`, which is a [recommended standard](https://peps.python.org/pep-0621/) 126 | * adopts the [`src` layout](https://browniebroke.com/blog/convert-existing-poetry-to-src-layout/), to avoid [common problems](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/) 127 | * keeps the actual code (`src`) and test code (`tests`) separated 128 | 129 | The `pyproject.toml` is the **main configuration file** for the project. It contains both 130 | general information about the software as well as configuration for various tools. 131 | 132 | In older software, most of this information is often scattered over many little 133 | tool-specific configuration files and a `setup.py`, `setup.cfg` and/or `requirements.txt` 134 | file. 135 | 136 | !!! tip 137 | `pyproject.toml` is the first place your should check 138 | when looking for the configuration of some development tool. 139 | 140 | ### Configuration 141 | 142 | The main tool needed to manage and configure the project is [Poetry](https://python-poetry.org). 143 | 144 | **Please follow its setup documentation to install it correctly.** Poetry should **not** 145 | be installed with `pip` like other Python tools. 146 | 147 | Poetry performs many important tasks: 148 | 149 | * it manages the **virtual environment(s)** used for the project 150 | * it manages all the **dependencies** needed for the code to work 151 | * it takes care of **packaging** the code into a `pip`-installable package 152 | 153 | You can find a cheatsheet with the most important commands 154 | [here](https://vikasz.hashnode.dev/python-poetry-cheatsheet) 155 | and consult its official documentation for detailed information. 156 | 157 | Note that `poetry` is only needed for development of the repository. 158 | The *end-users* who just want to *install and use* this project 159 | do **not need** to set up or know anything about poetry. 160 | 161 | !!! tip 162 | If you use `poetry shell` to activate the virtual environment of the project, 163 | and the project is already installed with `poetry install`, in the following you do not 164 | have to prepend `poetry run` in the commands you will see below. 165 | 166 | ### Task Runner 167 | 168 | It is a good practice to have a common way for launching different project-related tasks. 169 | It removes the need of remembering flags for various tools, and avoids duplication 170 | of the same commands in the CI pipelines. If something in a workflow needs to change, 171 | it can be changed in just one place, thus reducing the risk of making a mistake. 172 | 173 | Often projects use a shell script or `Makefile` for this purpose. This project uses 174 | [poethepoet](https://github.com/nat-n/poethepoet), as it integrates nicely with `poetry`. 175 | The tasks are defined in `pyproject.toml` and can be launched using: 176 | 177 | ```bash 178 | poetry run poe TASK_NAME 179 | ``` 180 | 181 | ### CI Workflows 182 | 183 | The project contains CI workflows for both GitHub and GitLab. 184 | 185 | The main CI pipeline runs on each new pushed commit and will 186 | 187 | 1. Run all configured code analysis tools, 188 | 2. Run code tests with multiple versions of Python, 189 | 3. build and deploy the online project documentation website, and 190 | 4. *if a new version tag was pushed,* launch the release workflow 191 | 192 | ## Quality Control 193 | 194 | ### Static Analysis 195 | 196 | Except for code testing, most tools for quality control are added to the project as 197 | [`pre-commit`](https://pre-commit.com/) *hooks*. The `pre-commit` tool takes care of 198 | installing, updating and running the tools according to the configuration in the 199 | `.pre-commit-config.yaml` file. 200 | 201 | For every new copy of the repository (e.g. after `git clone`), `pre-commit` first must 202 | be activated. This is usually done using `pre-commit install`, which also requires that 203 | `pre-commit` is already available. For more convenience, we simplified the procedure. 204 | 205 | In this project, you can run: 206 | 207 | ```bash 208 | poetry run poe init-dev 209 | ``` 210 | 211 | This will make sure that `pre-commit` is enabled in your repository copy. 212 | 213 | Once enabled, 214 | **every time** you try to `git commit` some **changed files** 215 | various tools will run on those (and only those) files. 216 | 217 | This means that (with some exceptions) `pre-commit` by default will run only 218 | on the changed files that were added to the next commit 219 | (i.e., files in the git *staging area*). 220 | These files are usually colored in *green* when running `git status`. 221 | 222 | * Some tools only *report* the problems they detected 223 | * Some tools actively *modify* files (e.g., fix formatting) 224 | 225 | In any case, the `git commit` will **fail** if a file was modified by a tool, or some 226 | problems were reported. In order to complete the commit, you need to 227 | 228 | * resolve all problems (by fixing them or marking them as false alarm), and 229 | * `git add` all changed files **again** (to update the files in the *staging area*). 230 | 231 | After doing that, you can retry to `git commit` your changes. 232 | 233 | To avoid having to deal with many issues at once, it is a good habit to run 234 | `pre-commit` by hand from time to time. In this project, this can be done with: 235 | 236 | ```bash 237 | poetry run poe lint --all-files 238 | ``` 239 | 240 | ### Testing 241 | 242 | [pytest](https://docs.pytest.org/en/7.3.x/) is used as the main framework for testing. 243 | 244 | The project uses the [`pytest-cov`](https://pytest-cov.readthedocs.io/en/latest/) plugin 245 | to integrate `pytest` with 246 | [`coverage`](https://coverage.readthedocs.io/en/latest/), which 247 | collects and reports test coverage information. 248 | 249 | In addition to writing regular unit tests with `pytest`, consider using 250 | [hypothesis](https://hypothesis.readthedocs.io/en/latest/index.html), 251 | which integrates nicely with `pytest` and implements *property-based testing* - which 252 | involves automatic generation of randomized inputs for test cases. This can help to find 253 | bugs often found for various edge cases that are easy to overlook in ad-hoc manual tests. 254 | Such randomized tests can be a good addition to hand-crafted tests and inputs. 255 | 256 | To run all tests, either invoke `pytest` directly, or use the provided task: 257 | 258 | ```bash 259 | poetry run poe test 260 | ``` 261 | 262 | !!! tip 263 | Add the flag `--cov` to enable the test coverage tracking and get a table with 264 | results after the tests are completed. 265 | 266 | ## Documentation 267 | 268 | The project uses [`mkdocs`](https://www.mkdocs.org/) with the popular and excellent 269 | [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material/) 270 | theme to generate the project documentation website, which provides both **user and 271 | developer documentation**. 272 | 273 | `mkdocs` is configured in the `mkdocs.yml` file, which we prepared in a way that there is 274 | 275 | * **no need to duplicate sections** from files in other places (such as `README.md`) 276 | * fully **automatic API documentation** pages based on Python docstrings in the code 277 | * a detailed **test coverage report** is included in the website 278 | 279 | The first point is important, because avoiding duplication means avoiding errors 280 | whenever text or examples are updated. 281 | The second point is convenient, as modules and functions do not need to 282 | be added by hand, which is easy to forget. 283 | The third point removes the need to use an external service such as 284 | [CodeCov](https://about.codecov.io/) to store and present code coverage information. 285 | 286 | As software changes over time and users cannot always keep up with the latest developments, 287 | each new version of the software should provide version-specific documentation. 288 | To make this both possible as well as convenient, this project uses 289 | [`mike`](https://github.com/jimporter/mike) to generate and manage the `mkdocs` 290 | **documentation for different versions** of the software. 291 | 292 | !!! tip 293 | You can easily add new pages (e.g. extended tutorials or topic-specific guides) to 294 | your documentation website by creating markdown files in the `docs/` directory and 295 | adding them to the `nav` section in `mkdocs.yml`. 296 | 297 | ### Offline Documentation 298 | 299 | You can manually generate a local and fully offline copy of the documentation, which 300 | can be useful for e.g. previewing the results during active work on the documentation: 301 | 302 | ```bash 303 | poetry install --with docs 304 | poetry run poe docs 305 | ``` 306 | 307 | Once the documentation site is built, run `mkdocs serve` and 308 | open `https://localhost:8000` in your browser to see the local copy of the website. 309 | 310 | !!! tip 311 | You probably should always check bigger website updates locally before it is publicly 312 | deployed. The automatic pipelines can only catch technical problems, but you still 313 | e.g. might want to do some proof-reading. 314 | 315 | ### Online Documentation 316 | 317 | To avoid dependence on additional services such as [readthedocs](https://readthedocs.org/), 318 | the project website is set up for simple deployment using 319 | [GitHub Pages](https://pages.github.com/) or 320 | [GitLab Pages](https://docs.gitlab.com/ee/user/project/pages/). 321 | 322 | The provided CI pipeline automatically generates the documentation for the latest 323 | development version (i.e., current state of the `main` branch) as well as every released 324 | version (i.e., marked by a version tag `vX.Y.Z`). 325 | 326 | Publishing the documentation to a website using GitHub or GitLab Pages needs a bit 327 | of configuration. Please follow the steps for your respective hosting service. 328 | 329 | === "GitLab" 330 | 1. Create a new *project access token* for GitLab Pages deployment 331 | - in your GitLab project, go to **Settings > Access Tokens** 332 | - Add a **new token** with the following settings: 333 | - **Token name:** `PAGES_DEPLOYMENT_TOKEN` 334 | - **Expiration date:** *(far in the future)* 335 | - **Select a role:** *Maintainer* 336 | - **Select scopes:** *read_repository, write_repository* 337 | 2. Provide the token as a masked**(!)** variable to the CI pipeline 338 | - in your GitLab project, go to **Settings > CI/CD** 339 | - in the section **Variables** add a new variable with 340 | - **Key:** `PAGES_TOKEN` 341 | - **Value:** *(the token string, as generated in the previous step)* 342 | - enable **Mask variable**, so your token will not appear in logs 343 | 3. Ensure that the GitLab pages URL is correct 344 | - in your GitLab project, go to **Deploy > Pages** 345 | - make sure that *Use unique domain* is **NOT** enabled 346 | - check that under *Access pages* the URL matches the `site_url` in your `mkdocs.yml` 347 | 348 | === "GitHub" 349 | - make sure that you pushed the repository and the CI pipeline completed at least once 350 | - check that a `gh-pages` branch exists (created by the CI) 351 | - go to your GitHub repository **Settings** and from there to settings for **Pages** 352 | - under **Build and deployment** pick `gh-pages` as the branch for serving documentation 353 | 354 | !!! warning "Important Information" 355 | When adding any kind of **token** to your repository configuration, 356 | which usually allows code and pipelines to access and modify your project, 357 | make sure that the token is protected. 358 | 359 | * In GitHub, tokens should be always added as [**secrets**](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) 360 | * In GitLab, tokens should be added as CI variables that are [**masked**](https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable) 361 | 362 | This will make sure that the token will not appear in logs of the CI pipeline runs 363 | and minimize the risk of abuse for malicious purposes. 364 | **NEVER save a token in a text file in your repository!** 365 | 366 | !!! tip 367 | Should anything go wrong and you need to manually access the data of the deployed 368 | website, you can find it in the `gh-pages` or `gl-pages` branch of the repository. 369 | Normally you should not need to use that branch directly, though. 370 | 371 | ## Releases 372 | 373 | From time to time the project is ready for a new **release** for users. 374 | 375 | ### Creating a New Release 376 | 377 | Before releasing a new version, push the commit the new release should be based on 378 | to the upstream repository, and make sure that: 379 | 380 | * the CI pipeline completes successfully 381 | * the version number in `pyproject.toml` is updated, in particular: 382 | * it must be larger than the previous released version 383 | * it should adequately reflect the [severity of changes](https://semver.org) 384 | * the provided user and developer documentation is up-to-date, including: 385 | * a new section in the `CHANGELOG.md` file summarizing changes in the new version 386 | * possibly revised information about contributors and/or maintainers 387 | 388 | If this is the case, proceed with the release by: 389 | 390 | * creating a new tag that matches the version in the `pyproject.toml`: `git tag vX.Y.Z` 391 | * pushing the new tag to the upstream repository: `git push origin vX.Y.Z` 392 | 393 | The pushed version tag will trigger a pipeline that will: 394 | 395 | * build and deploy the documentation website for the specific version 396 | * publish the package to enabled targets (see below) 397 | 398 | ### Release Targets 399 | 400 | The CI pipelines are built in such a way that features can be enabled, disabled and configured easily. 401 | 402 | === "GitLab" 403 | Targets for releases can be enabled or disabled in the `variables` section in `.gitlab-ci.yml`. 404 | 405 | === "GitHub" 406 | Targets for releases can be enabled or disabled in `.github/workflows/ci.yml` and 407 | configured by adapting the corresponding actions in `.github/workflows/releases.yml`. 408 | 409 | #### GitHub / GitLab Release 410 | 411 | By default, the release workflow will create a basic GitHub or GitLab Release that provides 412 | a snapshot of the repository as a download. This requires no additional configuration. 413 | 414 | See [here](https://github.com/softprops/action-gh-release) 415 | for information on how the Github release can be customized. 416 | 417 | !!! note 418 | The Github Release can be used to trigger automated software publication of your 419 | released versions to 420 | [Zenodo](https://docs.github.com/en/repositories/archiving-a-github-repository/referencing-and-citing-content), 421 | based on the metadata provided in the `CITATION.cff` file. 422 | 423 | #### PyPI and Compatible Indices 424 | 425 | The CI pipelines support automatic releases to PyPI, Test PyPI or other custom repositories, 426 | but in any case this requires a bit of initial configuration. 427 | 428 | === "GitLab" 429 | For automated releases to PyPI and Test PyPI the project uses the classic token-based workflow. 430 | 431 | Before the project can be released to PyPI or Test PyPI the first time, 432 | a new PyPI API token must be created in the PyPI account of the main project maintainer, 433 | and added to your CI as a masked variable, and a variable updated in the `.gitlab-ci.yml`. 434 | 435 | The corresponding tokens can be added analogously to the `PAGES_TOKEN` for online documentation, 436 | which was explained [here](#online-documentation). 437 | 438 | **PyPI:** 439 | 440 | - add the token as a masked CI variable called `RELEASE_TOKEN_pypi` 441 | - in `.gitlab-ci.yml`, set `release_to_pypi: "true"` 442 | 443 | **Test PyPI:** 444 | 445 | - add the token as a masked CI variable called `RELEASE_TOKEN_testpypi` 446 | - in `.gitlab-ci.yml`, set `release_to_testpypi: "true"` 447 | 448 | **Custom Package Index:** 449 | 450 | - add the token as a masked CI variable called `RELEASE_TOKEN_custom` 451 | - in `.gitlab-ci.yml`, set `release_to_custom: "true"` 452 | - update `PKGIDX_URL` in the `release_custom_pypi` job to the correct 453 | [legacy API](https://python-poetry.org/docs/repositories/#publishable-repositories) endpoint 454 | 455 | === "GitHub" 456 | For automated releases to PyPI and Test PyPI the project uses the new 457 | [Trusted Publishers](https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/) 458 | workflow that is both more secure and convenient to use than other authorization methods. 459 | 460 | Before the project can be released to PyPI or Test PyPI the first time, 461 | first a [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) 462 | must be added in the PyPI account of the main project maintainer, **using 463 | `release.yml` as the requested _workflow name_**. 464 | 465 | !!! note 466 | It is important to use the correct workflow name, otherwise the workflow will fail! 467 | 468 | Once this is done, set the corresponding option (`to_pypi` / `to_test_pypi`) to `true` 469 | in the `publish` job in `ci.yml` to enable the corresponding publication target. 470 | 471 | If the old and less secure token-based authentication method is needed or 472 | the package should be published to a different PyPI-compatible package index, please 473 | adapt `release.yml` [accordingly](https://github.com/pypa/gh-action-pypi-publish). 474 | 475 | If for some reason you do not want to use the CI for the PyPI releases, you can skip these instructions 476 | and manually use [`poetry publish`](https://python-poetry.org/docs/cli/#publish) to do the release. 477 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/index.md: -------------------------------------------------------------------------------- 1 | --8<-- "README.md:abstract" 2 | 3 | ## Usage 4 | 5 | To get started, please check out the [quickstart guide](./quickstart.md). 6 | 7 | --8<-- "README.md:citation" 8 | 9 | --8<-- "README.md:acknowledgements" 10 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/license.md: -------------------------------------------------------------------------------- 1 | Unless stated otherwise, all code provided by this project 2 | (excluding external dependencies) is distributed under the following license: 3 | 4 | ``` 5 | --8<-- "LICENSE" 6 | ``` 7 | 8 | This project is [REUSE](https://reuse.software/) compliant. 9 | The following detailed license and copyright information in 10 | [DEP5](https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/) 11 | format can also be found in the `REUSE.toml` file in the project source directory: 12 | 13 | ``` 14 | --8<-- "REUSE.toml" 15 | ``` 16 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block outdated %} 4 | This page is for an outdated or development version. 5 | 6 | Click here to go to the latest stable version. 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/overrides/partials/copyright.html: -------------------------------------------------------------------------------- 1 | 2 | 27 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/quickstart.md: -------------------------------------------------------------------------------- 1 | --8<-- "README.md:quickstart" 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/scripts/coverage_status.py: -------------------------------------------------------------------------------- 1 | """Mkdocs hook to run tests with coverage collection and generate a badge.""" 2 | 3 | import logging 4 | from io import StringIO 5 | from pathlib import Path 6 | 7 | import anybadge 8 | import pytest 9 | from coverage import Coverage 10 | 11 | log = logging.getLogger("mkdocs") 12 | 13 | 14 | badge_colors = { 15 | 20.0: "red", 16 | 40.0: "orange", 17 | 60.0: "yellow", 18 | 80.0: "greenyellow", 19 | 90.0: "green", 20 | } 21 | """Colors for overall coverage percentage (0-100).""" 22 | 23 | 24 | def on_pre_build(config): # noqa 25 | """Generate coverage report if it is missing and create a badge.""" 26 | if not Path("htmlcov").is_dir() or not Path(".coverage").is_file(): 27 | log.info("Missing htmlcov or .coverage, running pytest to collect.") 28 | pytest.main(["--cov", "--cov-report=html"]) 29 | else: 30 | log.info("Using existing coverage data.") 31 | 32 | cov = Coverage() 33 | cov.load() 34 | cov_percent = int(cov.report(file=StringIO())) 35 | log.info(f"Test Coverage: {cov_percent}%, generating badge.") 36 | 37 | badge = anybadge.Badge( 38 | "coverage", 39 | cov_percent, 40 | value_prefix=" ", 41 | value_suffix="% ", 42 | thresholds=badge_colors, 43 | ) 44 | 45 | badge_svg = Path("docs/coverage_badge.svg") 46 | if badge_svg.is_file(): 47 | badge_svg.unlink() 48 | badge.write_badge(badge_svg) 49 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/docs/scripts/gen_ref_pages.py: -------------------------------------------------------------------------------- 1 | """Generate the code reference pages. 2 | 3 | See: https://mkdocstrings.github.io/recipes/ 4 | """ 5 | 6 | from pathlib import Path 7 | 8 | import mkdocs_gen_files 9 | 10 | nav = mkdocs_gen_files.Nav() 11 | 12 | for path in sorted(Path("src").rglob("*.py")): 13 | module_path = path.relative_to("src").with_suffix("") 14 | doc_path = path.relative_to("src").with_suffix(".md") 15 | full_doc_path = Path("reference", doc_path) 16 | 17 | parts = list(module_path.parts) 18 | 19 | if parts[-1] == "__init__": 20 | parts = parts[:-1] 21 | doc_path = doc_path.with_name("index.md") 22 | full_doc_path = full_doc_path.with_name("index.md") 23 | elif parts[-1] == "__main__": 24 | continue 25 | 26 | nav[parts] = doc_path.as_posix() 27 | 28 | with mkdocs_gen_files.open(full_doc_path, "w") as fd: 29 | identifier = ".".join(parts) 30 | print("::: " + identifier, file=fd) 31 | 32 | mkdocs_gen_files.set_edit_path(full_doc_path, path) 33 | 34 | with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: 35 | nav_file.writelines(nav.build_literate_nav()) 36 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # basic configuration: 2 | site_name: "{{ cookiecutter.project_name }}" 3 | site_description: "{{ cookiecutter.project_description }}" 4 | repo_name: "{{ cookiecutter.project_slug }}" 5 | 6 | site_url: "{{ cookiecutter.project_pages_url }}" 7 | repo_url: "{{ cookiecutter.project_repo_url }}" 8 | 9 | # add support for buttons to edit a documentation page/section (both github and gitlab): 10 | edit_uri: "edit/main/docs/" 11 | 12 | # navigation structure (TOC + respective markdown files): 13 | nav: 14 | - Home: 15 | - Overview: index.md 16 | - Changelog: changelog.md 17 | - Credits: credits.md 18 | - License: license.md 19 | - Usage: 20 | - Quickstart: quickstart.md 21 | - API: reference/ # auto-generated (from Python docstrings) 22 | - Development: 23 | - How To Contribute: contributing.md 24 | - Developer Guide: dev_guide.md 25 | - Code of Conduct: code_of_conduct.md 26 | - Coverage Report: coverage.md # cov report (pytest --cov --cov-report html) 27 | 28 | extra: 29 | # social links in footer: https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-footer/?h=social#social-links 30 | social: 31 | # you can add more various social links, if desired 32 | # - icon: 'fontawesome/solid/globe' 33 | # link: 'https://your-organization-homepage.org' 34 | # - icon: 'fontawesome/brands/github' 35 | # link: 'https://github.com/your_github_organization' 36 | # - icon: 'fontawesome/brands/gitlab' 37 | # link: 'https://your.gitlab.instance/your_organization' 38 | 39 | # versioned docs: https://squidfunk.github.io/mkdocs-material/setup/setting-up-versioning/ 40 | version: 41 | provider: mike 42 | 43 | # optimization for offline usage: 44 | use_directory_urls: !ENV [OFFLINE, false] 45 | 46 | theme: 47 | # See here for customization guide: https://squidfunk.github.io/mkdocs-material/setup/ 48 | name: "material" 49 | custom_dir: "docs/overrides" 50 | 51 | features: 52 | - content.action.edit 53 | - content.action.view 54 | # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations 55 | - content.code.annotate 56 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-header/ 57 | - header.autohide 58 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/ 59 | - navigation.footer 60 | - navigation.instant 61 | - navigation.tabs 62 | - navigation.tabs.sticky 63 | - navigation.tracking 64 | - navigation.top 65 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/ 66 | - search.highlight 67 | - search.suggest 68 | 69 | # light/dark mode toggle: https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ 70 | palette: 71 | - media: "(prefers-color-scheme: light)" 72 | scheme: default 73 | toggle: 74 | icon: material/brightness-7 75 | name: Switch to dark mode 76 | - media: "(prefers-color-scheme: dark)" 77 | scheme: slate 78 | toggle: 79 | icon: material/brightness-4 80 | name: Switch to light mode 81 | 82 | extra_css: 83 | # list of extra CSS files to override and configure defaults: 84 | - css/style.css 85 | 86 | markdown_extensions: 87 | # Enable permalink to sections: 88 | - toc: 89 | permalink: true 90 | # Setting HTML/CSS attributes: https://python-markdown.github.io/extensions/attr_list/ 91 | - attr_list 92 | # Definitions: https://python-markdown.github.io/extensions/definition_lists/ 93 | - def_list 94 | # Footnotes: https://squidfunk.github.io/mkdocs-material/reference/footnotes/ 95 | - footnotes 96 | # Various boxes: https://squidfunk.github.io/mkdocs-material/reference/admonitions/ 97 | - admonition 98 | - pymdownx.details 99 | - pymdownx.superfences 100 | # smart links: https://facelessuser.github.io/pymdown-extensions/extensions/magiclink/ 101 | - pymdownx.magiclink: 102 | repo_url_shorthand: true 103 | # Superscript: https://facelessuser.github.io/pymdown-extensions/extensions/caret/ 104 | - pymdownx.caret 105 | # Strikethrough markup: https://facelessuser.github.io/pymdown-extensions/extensions/tilde/ 106 | - pymdownx.tilde 107 | # Auto-Unicode for common symbols: https://facelessuser.github.io/pymdown-extensions/extensions/smartsymbols/ 108 | - pymdownx.smartsymbols 109 | # Github-style task list: https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#tasklist 110 | - pymdownx.tasklist: 111 | custom_checkbox: true 112 | # Tabbed boxes: https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ 113 | - pymdownx.tabbed: 114 | alternate_style: true 115 | # Inlining markdown: https://facelessuser.github.io/pymdown-extensions/extensions/snippets/ 116 | - pymdownx.snippets: 117 | check_paths: true 118 | # Icons and Emoji: https://squidfunk.github.io/mkdocs-material/reference/icons-emojis/ 119 | - pymdownx.emoji: 120 | emoji_index: !!python/name:material.extensions.emoji.twemoji 121 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 122 | 123 | plugins: 124 | # default search box (must be listed if plugins are added) 125 | - search 126 | # embed coverage report: https://pawamoy.github.io/mkdocs-coverage/ 127 | - coverage 128 | # execute code (e.g. generate diagrams): https://pawamoy.github.io/markdown-exec/ 129 | - markdown-exec 130 | # automatic API docs: https://mkdocstrings.github.io/recipes/#automatic-code-reference-pages 131 | - gen-files: 132 | scripts: 133 | - docs/scripts/gen_ref_pages.py 134 | - literate-nav: 135 | nav_file: SUMMARY.md 136 | - section-index 137 | - mkdocstrings: 138 | handlers: 139 | python: 140 | paths: [src] 141 | options: 142 | members_order: source 143 | separate_signature: true 144 | show_signature_annotations: true 145 | # https://squidfunk.github.io/mkdocs-material/setup/building-for-offline-usage/#built-in-offline-plugin 146 | # To allow building for offline usage, e.g. with: OFFLINE=true mkdocs build 147 | - offline: 148 | enabled: !ENV [OFFLINE, false] 149 | # to make multi-version docs work right 150 | - mike 151 | 152 | hooks: 153 | - docs/scripts/coverage_status.py 154 | 155 | strict: true 156 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | # ---- DO NOT EDIT - core project metadata managed by somesy ---- 3 | # to update, edit values in [tool.somesy.project] section 4 | # and run somesy: poetry run somesy 5 | name = "{{ cookiecutter.project_slug }}" 6 | version = "{{ cookiecutter.project_version }}" 7 | description = "{{ cookiecutter.project_description }}" 8 | license = {text = "{{ cookiecutter.project_license }}"} 9 | 10 | authors = [{name = "{{ cookiecutter.author_first_name }} {{ cookiecutter.author_last_name }}", email = "{{ cookiecutter.author_email }}"}] 11 | maintainers = [{name = "{{ cookiecutter.author_first_name }} {{ cookiecutter.author_last_name }}", email = "{{ cookiecutter.author_email }}"}] 12 | 13 | requires-python = ">=3.9" 14 | keywords = {{ cookiecutter.project_keywords.split() | jsonify }} 15 | dependencies = [ 16 | {%- if cookiecutter.init_cli %} 17 | "typer[all]>=0.12.3", 18 | {%- endif %} 19 | {%- if cookiecutter.init_api %} 20 | "fastapi>=0.111.1", 21 | "uvicorn>=0.30.4", 22 | {%- endif %} 23 | ] 24 | 25 | # ---------------------------------------------------------------- 26 | 27 | # Python- and Poetry-specific metadata 28 | # ------------------------------------ 29 | readme = "README.md" 30 | classifiers = [ 31 | # TODO: update the classifier strings 32 | # (see https://pypi.org/classifiers/) 33 | "Operating System :: POSIX :: Linux", 34 | "Intended Audience :: Science/Research", 35 | "Intended Audience :: Developers", 36 | ] 37 | 38 | # ---- managed by somesy, see .somesy.toml ---- 39 | [project.urls] 40 | repository = "{{ cookiecutter.project_repo_url }}" 41 | homepage = "{{ cookiecutter.project_pages_url }}" 42 | documentation = "{{ cookiecutter.project_pages_url }}" 43 | 44 | [tool.poetry] 45 | # the Python packages that will be included in a built distribution: 46 | packages = [{include = "{{ cookiecutter.project_package }}", from = "src"}] 47 | 48 | # always include basic info for humans and core metadata in the distribution, 49 | # include files related to test and documentation only in sdist: 50 | include = [ 51 | "*.md", "LICENSE", "LICENSES", "REUSE.toml", "CITATION.cff", "codemeta.json", 52 | { path = "mkdocs.yml", format = "sdist" }, 53 | { path = "docs", format = "sdist" }, 54 | { path = "tests", format = "sdist" }, 55 | ] 56 | 57 | [tool.poetry.dependencies] 58 | python = ">=3.9,<4.0" 59 | 60 | [tool.poetry.group.dev.dependencies] 61 | poethepoet = "^0.27.0" 62 | pre-commit = "^3.5.0" 63 | pytest = "^8.3.2" 64 | pytest-cov = "^5.0.0" 65 | hypothesis = "^6.108.5" 66 | licensecheck = "^2024.2" 67 | {%- if cookiecutter.init_api %} 68 | httpx = "^0.27.0" 69 | {%- endif %} 70 | 71 | [tool.poetry.group.docs] 72 | optional = true 73 | 74 | [tool.poetry.group.docs.dependencies] 75 | mkdocs = "^1.6.0" 76 | mkdocstrings = {extras = ["python"], version = "^0.25.2"} 77 | mkdocs-material = "^9.5.30" 78 | mkdocs-gen-files = "^0.5.0" 79 | mkdocs-literate-nav = "^0.6.1" 80 | mkdocs-section-index = "^0.3.9" 81 | mkdocs-macros-plugin = "^1.0.5" 82 | markdown-include = "^0.8.1" 83 | pymdown-extensions = "^10.9" 84 | markdown-exec = {extras = ["ansi"], version = "^1.9.3"} 85 | mkdocs-coverage = "^1.1.0" 86 | mike = "^2.1.2" 87 | anybadge = "^1.14.0" 88 | interrogate = "^1.7.0" 89 | black = "^24.4.2" 90 | 91 | [project.scripts] 92 | # put your script entrypoints here 93 | # some-script = 'module.submodule:some_object' 94 | {%- if cookiecutter.init_cli %} 95 | {{ cookiecutter.project_slug }}-cli = '{{ cookiecutter.project_package }}.cli:app' 96 | {%- endif %} 97 | {%- if cookiecutter.init_api %} 98 | {{ cookiecutter.project_slug }}-api = '{{ cookiecutter.project_package }}.api:run' 99 | {%- endif %} 100 | 101 | [build-system] 102 | requires = ["poetry-core>=2.0"] 103 | build-backend = "poetry.core.masonry.api" 104 | 105 | # NOTE: You can run the following with "poetry poe TASK" 106 | [tool.poe.tasks] 107 | init-dev = "pre-commit install" # run once after clone to enable various tools 108 | lint = "pre-commit run" # pass --all-files to check everything 109 | test = "pytest" # pass --cov to also collect coverage info 110 | docs = "mkdocs build" # run this to generate local documentation 111 | licensecheck = "licensecheck" # run this when you add new deps 112 | 113 | # Tool Configurations 114 | # ------------------- 115 | [tool.pytest.ini_options] 116 | pythonpath = ["src"] 117 | addopts = "--cov-report=term-missing:skip-covered" 118 | filterwarnings = [ 119 | # Example: 120 | # "ignore::DeprecationWarning:importlib_metadata.*" 121 | ] 122 | 123 | [tool.coverage.run] 124 | source = ["{{ cookiecutter.project_package }}"] 125 | 126 | [tool.coverage.report] 127 | exclude_lines = [ 128 | "pragma: no cover", 129 | "def __repr__", 130 | "if self.debug:", 131 | "if settings.DEBUG", 132 | "raise AssertionError", 133 | "raise NotImplementedError", 134 | "if 0:", 135 | "if TYPE_CHECKING:", 136 | "if __name__ == .__main__.:", 137 | "class .*\\bProtocol\\):", 138 | "@(abc\\.)?abstractmethod", 139 | ] 140 | 141 | [tool.ruff.lint] 142 | # see here: https://docs.astral.sh/ruff/rules/ 143 | # ruff by default works like a mix of flake8, autoflake and black 144 | # we extend default linter rules to substitute: 145 | # flake8-bugbear, pydocstyle, isort, flake8-bandit 146 | extend-select = ["B", "D", "I", "S"] 147 | ignore = ["D203", "D213", "D407", "B008", "S101", "D102", "D103"] 148 | 149 | [tool.bandit] 150 | exclude_dirs = ["tests", "scripts"] 151 | 152 | [tool.licensecheck] 153 | using = "poetry" 154 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/somesy.toml: -------------------------------------------------------------------------------- 1 | # NOTE: this repository uses the tool somesy to help you easily maintain 2 | # and synchronize all the high-level project metadata across multiple files. 3 | # To see which other metadata can be added, check out the somesy documentation 4 | # https://materials-data-science-and-informatics.github.io/somesy/main/ 5 | [project] 6 | name = "{{ cookiecutter.project_slug }}" 7 | version = "{{ cookiecutter.project_version }}" 8 | description = "{{ cookiecutter.project_description }}" 9 | license = "{{ cookiecutter.project_license }}" 10 | 11 | keywords = {{ cookiecutter.project_keywords.split() | jsonify }} 12 | repository = "{{ cookiecutter.project_repo_url }}" 13 | homepage = "{{ cookiecutter.project_pages_url }}" 14 | documentation = "{{ cookiecutter.project_pages_url }}" 15 | 16 | [[project.people]] 17 | given-names = "{{ cookiecutter.author_first_name }}" 18 | family-names = "{{ cookiecutter.author_last_name }}" 19 | email = "{{ cookiecutter.author_email }}" 20 | {%- if cookiecutter.author_orcid_url %} 21 | orcid = "{{ cookiecutter.author_orcid_url }}" 22 | {%- else %} 23 | # orcid = "https://orcid.org/your-orcid" 24 | {%- endif %} 25 | author = true 26 | maintainer = true 27 | 28 | # You also can add more authors, maintainers or contributors here: 29 | # [[project.people]] 30 | # given-names = "Another" 31 | # family-names = "Contributor" 32 | # email = "contributor@email.com" 33 | # orcid = "https://orcid.org/0123-4567-8910-1112" 34 | # ... 35 | 36 | [config] 37 | verbose = true 38 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_package }}/__init__.py: -------------------------------------------------------------------------------- 1 | """Top level module of the project.""" 2 | 3 | from importlib.metadata import version 4 | from typing import Final 5 | 6 | # Set version, it will use version from pyproject.toml if defined 7 | __version__: Final[str] = version(__package__ or __name__) 8 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_package }}/api.py: -------------------------------------------------------------------------------- 1 | """API of {{ cookiecutter.project_slug }}.""" 2 | from fastapi import FastAPI, HTTPException 3 | 4 | from {{ cookiecutter.project_package }}.lib import CalcOperation, calculate 5 | 6 | app = FastAPI() 7 | 8 | 9 | @app.get("/calculate/{op}") 10 | def calc(op: CalcOperation, x: int, y: int = 0): 11 | """Return result of calculation on two integers.""" 12 | try: 13 | return calculate(op, x, y) 14 | 15 | except (ZeroDivisionError, ValueError, NotImplementedError) as e: 16 | if isinstance(e, ZeroDivisionError): 17 | err = f"Cannot divide x={x} by y=0!" 18 | else: 19 | err = str(e) 20 | raise HTTPException(status_code=422, detail=err) from e 21 | 22 | def run(): 23 | import uvicorn 24 | uvicorn.run(app, host="127.0.0.1", port=8000) 25 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_package }}/cli.py: -------------------------------------------------------------------------------- 1 | """CLI of {{ cookiecutter.project_slug }}.""" 2 | 3 | import typer 4 | 5 | from {{ cookiecutter.project_package }}.lib import CalcOperation, calculate 6 | 7 | # create subcommand app 8 | say = typer.Typer() 9 | 10 | # create main app 11 | app = typer.Typer() 12 | app.add_typer(say, name="say") 13 | 14 | # ---- 15 | 16 | 17 | @app.command() 18 | def calc(op: CalcOperation, x: int, y: int): 19 | """Compute the result of applying an operation on x and y.""" 20 | result: int = calculate(op, x, y) 21 | typer.echo(f"Result: {result}") 22 | 23 | 24 | # ---- 25 | 26 | 27 | @say.command() 28 | def hello(name: str): 29 | """Greet a person.""" 30 | print(f"Hello {name}") 31 | 32 | 33 | @say.command() 34 | def goodbye(name: str, formal: bool = False): 35 | """Say goodbye to a person.""" 36 | if formal: 37 | print(f"Goodbye {name}. Have a good day.") 38 | else: 39 | print(f"Bye {name}!") 40 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_package }}/lib.py: -------------------------------------------------------------------------------- 1 | """Core functionality of {{ cookiecutter.project_slug }}. 2 | 3 | This module can be used directly, or the functionality can be 4 | exposed using some other interface, such as CLI, GUI or an API. 5 | """ 6 | 7 | from enum import Enum 8 | 9 | 10 | class CalcOperation(str, Enum): 11 | """Supported operations of `calculate`.""" 12 | 13 | add = "add" 14 | multiply = "multiply" 15 | subtract = "subtract" 16 | divide = "divide" 17 | power = "power" 18 | 19 | 20 | def calculate(op: CalcOperation, x: int, y: int): 21 | """Calculate result of an operation on two integer numbers.""" 22 | if not isinstance(op, CalcOperation): 23 | raise ValueError(f"Unknown operation: {op}") 24 | 25 | if op == CalcOperation.add: 26 | return x + y 27 | elif op == CalcOperation.multiply: 28 | return x * y 29 | elif op == CalcOperation.subtract: 30 | return x - y 31 | elif op == CalcOperation.divide: 32 | return x // y 33 | 34 | err = f"Operation {op} is not implemented!" 35 | raise NotImplementedError(err) 36 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/temp-REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "{{ cookiecutter.project_slug }}" 3 | SPDX-PackageSupplier = "{{ cookiecutter.author_name_email }}" 4 | SPDX-PackageDownloadLocation = "{{ cookiecutter.project_repo_url }}" 5 | 6 | [[annotations]] 7 | path = [".gitignore", "pyproject.toml", "poetry.lock", ".pre-commit-config.yaml", "codemeta.json", "CITATION.cff", "README.md", "RELEASE_NOTES.md", "CHANGELOG.md", "CODE_OF_CONDUCT.md", "AUTHORS.md", "CONTRIBUTING.md", ".gitlab-ci.yml", ".gitlab/**", ".github/**", "mkdocs.yml", "docs/**", "somesy.toml"] 8 | precedence = "aggregate" 9 | SPDX-FileCopyrightText = "{{ cookiecutter.copyright_text }}" 10 | SPDX-License-Identifier = "CC0-1.0" 11 | 12 | [[annotations]] 13 | path = ["src/{{ cookiecutter.project_package }}/**", "tests/**"] 14 | precedence = "aggregate" 15 | SPDX-FileCopyrightText = "{{ cookiecutter.copyright_text }}" 16 | SPDX-License-Identifier = "{{ cookiecutter.project_license }}" 17 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for {{ cookiecutter.project_slug }}.""" 2 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Global pytest configuration.""" 2 | 3 | # NOTE: you can put your pytest fixtures here. 4 | # 5 | # Fixtures are very useful for automating repetitive test preparations 6 | # (such as accessing resources or creating test input files) 7 | # which are needed in multiple similar tests. 8 | # 9 | # see: https://docs.pytest.org/en/6.2.x/fixture.html 10 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/tests/test_api.py: -------------------------------------------------------------------------------- 1 | """Test API.""" 2 | from fastapi.testclient import TestClient 3 | 4 | from {{ cookiecutter.project_package }}.api import app 5 | 6 | client = TestClient(app) 7 | 8 | 9 | def test_calculate(): 10 | response = client.get("/calculate/divide?x=5&y=2") 11 | assert response.status_code == 200 12 | assert response.json() == 2 # int division 13 | 14 | response = client.get("/calculate/divide?x=5&y=0") 15 | assert response.status_code == 422 16 | assert "y=0" in response.json()["detail"] # division by 0 17 | 18 | response = client.get("/calculate/add?x=3.14") 19 | assert response.status_code == 422 # float input 20 | 21 | response = client.get("/calculate/power?x=5") 22 | assert response.status_code == 422 # unsupported op 23 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """Tests for the CLI.""" 2 | 3 | import pytest 4 | from hypothesis import given 5 | from hypothesis import strategies as st 6 | from typer.testing import CliRunner 7 | 8 | from {{ cookiecutter.project_package }}.cli import app 9 | 10 | runner = CliRunner() 11 | 12 | 13 | def test_calc_addition(): 14 | result = runner.invoke(app, ["calc", "add", "20", "22"]) 15 | 16 | assert result.exit_code == 0 17 | assert result.stdout.strip() == "Result: 42" 18 | 19 | 20 | person_names = ["Jane", "John"] 21 | 22 | 23 | @pytest.mark.parametrize( 24 | "name, formal", 25 | [(name, formal) for name in person_names for formal in [False, True]], 26 | ) 27 | def test_goodbye(name: str, formal: bool): 28 | args = ["say", "goodbye", name] 29 | if formal: 30 | args += ["--formal"] 31 | 32 | result = runner.invoke(app, args) 33 | 34 | assert result.exit_code == 0 35 | assert name in result.stdout 36 | if formal: 37 | assert "good day" in result.stdout 38 | 39 | 40 | # Example of hypothesis auto-generated inputs, 41 | # here the names are generated from a regular expression. 42 | 43 | # NOTE: this is not really a good regex for names! 44 | person_name_regex = r"^[A-Z]\w+$" 45 | 46 | 47 | @given(st.from_regex(person_name_regex)) 48 | def test_hello(name: str): 49 | result = runner.invoke(app, ["say", "hello", name]) 50 | 51 | assert result.exit_code == 0 52 | assert f"Hello {name}" in result.stdout 53 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/template/{{ cookiecutter.project_slug }}/tests/test_lib.py: -------------------------------------------------------------------------------- 1 | """Test for core library.""" 2 | 3 | import pytest 4 | from hypothesis import assume, given 5 | from hypothesis import strategies as st 6 | 7 | from {{ cookiecutter.project_package }}.lib import CalcOperation, calculate 8 | 9 | 10 | def test_calculate_invalid(): 11 | with pytest.raises(ZeroDivisionError): 12 | calculate(CalcOperation.divide, 123, 0) 13 | 14 | with pytest.raises(ValueError): 15 | calculate("invalid", 123, 0) # type: ignore 16 | 17 | with pytest.raises(NotImplementedError): 18 | calculate(CalcOperation.power, 2, 3) 19 | 20 | 21 | # Example of how hypothesis can be used to generate different 22 | # combinations of inputs automatically: 23 | 24 | 25 | @given(st.sampled_from(CalcOperation), st.integers(), st.integers()) 26 | def test_calculate(op, x, y): 27 | # assume can be used to ad-hoc filter outputs - 28 | # if the assumption is violated, the test instance is skipped. 29 | # (better: use strategy combinators for filtering) 30 | assume(op != CalcOperation.divide or y != 0) 31 | assume(op != CalcOperation.power) # not supported 32 | 33 | # we basically just check that there is no exception and the type is right 34 | result = calculate(op, x, y) 35 | assert isinstance(result, int) 36 | -------------------------------------------------------------------------------- /src/fair_python_cookiecutter/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for creation of template repository instances.""" 2 | 3 | import json 4 | import platform 5 | import shutil 6 | import subprocess 7 | import sys 8 | from pathlib import Path 9 | from typing import Optional 10 | from uuid import uuid1 11 | 12 | from importlib_resources import files 13 | from platformdirs import user_runtime_path 14 | 15 | from .config import CookiecutterJson 16 | 17 | 18 | class TempDir: 19 | """Cross-platform temporary directory.""" 20 | 21 | path: Path 22 | 23 | def __init__(self, prefix_dir: Optional[Path] = None, *, keep: bool = False): 24 | """Create a cross-platform temporary directory.""" 25 | dir: Path = prefix_dir or user_runtime_path(ensure_exists=True) 26 | if dir and not dir.is_dir(): 27 | raise ValueError( 28 | "Passed directory path does not exist or is not a directory!" 29 | ) 30 | 31 | self.path = dir / f"template_{uuid1()}" 32 | self.path.mkdir() 33 | self.keep = keep 34 | 35 | def __enter__(self): 36 | """Enter context manager.""" 37 | return self.path 38 | 39 | def __exit__(self, type, value, traceback): 40 | """Exit context manager.""" 41 | if not self.keep: 42 | shutil.rmtree(self.path) 43 | 44 | 45 | TEMPLATE_DIR = files("fair_python_cookiecutter") / "template" 46 | 47 | 48 | def copy_template( 49 | tmp_dir: Optional[Path] = None, *, cookiecutter_json: CookiecutterJson = None 50 | ) -> Path: 51 | """Create final template based on given configuration, returns template root directory.""" 52 | # if no config is given, use dummy default values (useful for testing) 53 | if not cookiecutter_json: 54 | with open(TEMPLATE_DIR / "cookiecutter.json", "r") as ccjson: 55 | ccjson_dct = json.load(ccjson) 56 | cookiecutter_json = CookiecutterJson.model_validate(ccjson_dct) 57 | 58 | # copy the meta-template (we do not fully hardcode the paths for robustness) 59 | template_root = None 60 | for path in TEMPLATE_DIR.glob("*"): 61 | trg_path = tmp_dir / path.name 62 | if path.is_dir(): 63 | if path.name.startswith("{{ cookiecutter"): 64 | template_root = path 65 | 66 | shutil.copytree(path, trg_path) 67 | else: 68 | shutil.copyfile(path, trg_path) 69 | 70 | # write a fresh cookiecutter.json based on user configuration 71 | with open(tmp_dir / "cookiecutter.json", "w", encoding="utf-8") as f: 72 | f.write(cookiecutter_json.model_dump_json(indent=2, by_alias=True)) 73 | 74 | if not template_root: 75 | raise RuntimeError( 76 | "Template root directory not identified, this must be a bug!" 77 | ) 78 | return template_root 79 | 80 | 81 | def get_venv_path() -> Optional[Path]: 82 | """Return path of venv, if we detect being inside one.""" 83 | return Path(sys.prefix) if sys.base_prefix != sys.prefix else None 84 | 85 | 86 | VENV_PATH: Optional[Path] = get_venv_path() 87 | """If set, the path of the virtual environment this tool is running in.""" 88 | 89 | 90 | def venv_activate_cmd(venv_path: Path): 91 | if platform.system() != "Windows": 92 | return "source " + str(venv_path / "bin" / "activate") 93 | else: 94 | return str(venv_path / "Scripts" / "activate") 95 | 96 | 97 | def run_cmd(cmd: str, cwd: Path = None): 98 | subprocess.run(cmd.split(), cwd=cwd, check=True) # noqa: S603 99 | -------------------------------------------------------------------------------- /tests/demo.yaml: -------------------------------------------------------------------------------- 1 | fair_python_cookiecutter: 2 | last_name: Pirogov 3 | first_name: Anton 4 | email: a.pirogov@fz-juelich.de 5 | orcid: 0000-0002-5077-7497 6 | 7 | affiliation: "Forschungszentrum Jülich GmbH - Institute for Materials Data Science and Informatics (IAS9)" 8 | copyright_holder: "Forschungszentrum Jülich GmbH - Institute for Materials Data Science and Informatics (IAS9) - Stefan Sandfeld " 9 | 10 | project_license: "Unlicense" 11 | project_version: "0.1.2" 12 | project_description: TODO - add description 13 | project_keywords: TODO add keywords 14 | 15 | init_cli: true 16 | init_api: true 17 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | import pytest 5 | 6 | from fair_python_cookiecutter.main import create_repository, CookiecutterConfig 7 | 8 | 9 | def test_configured(tmp_path): 10 | # generate with all demo code and different license 11 | conf = CookiecutterConfig.load(config_file="./tests/demo.yaml") 12 | conf.fair_python_cookiecutter.infer_from_repo_url( 13 | "https://github.com/MyOrg/my-project" 14 | ) 15 | DEMO_PROJ_PKG = "my_project" 16 | dir = create_repository(conf, tmp_path) 17 | 18 | # should have the code files 19 | assert (dir / f"src/{DEMO_PROJ_PKG}/api.py").is_file() 20 | assert (dir / f"src/{DEMO_PROJ_PKG}/cli.py").is_file() 21 | assert (dir / "tests/test_api.py").is_file() 22 | assert (dir / "tests/test_cli.py").is_file() 23 | # and the expected license (Unlicense) 24 | first_license_line = open(dir / "LICENSE", "r").readline() 25 | assert first_license_line.find("public domain") > 0 26 | 27 | 28 | # TODO: sanity-check after generation that all main tasks work locally: 29 | # poetry install --with docs 30 | # poetry run poe lint --all-files 31 | # poetry run poe test 32 | # poetry run poe docs 33 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from fair_python_cookiecutter.utils import TempDir, copy_template 3 | 4 | import pytest 5 | 6 | 7 | def test_tempdir(tmp_path): 8 | filename = "test.txt" 9 | 10 | with TempDir() as dir: 11 | assert dir.is_dir() 12 | 13 | with open(dir / filename, "w") as f: 14 | f.write("test") 15 | 16 | # with defaults should cleanup by default 17 | assert not dir.exists() 18 | 19 | with TempDir(tmp_path, keep=True) as dir: 20 | with open(dir / filename, "w") as f: 21 | f.write("test") 22 | 23 | # with keep=True dir should not be removed 24 | assert (dir / filename).is_file() 25 | 26 | # existing prefix path is used 27 | with TempDir(tmp_path) as dir: 28 | assert dir.parent == tmp_path 29 | 30 | # non-existing prefix path raises ValueError 31 | with pytest.raises(ValueError): 32 | dir = TempDir(tmp_path / "invalid") 33 | 34 | 35 | def test_copy_template(tmp_path): 36 | with TempDir(tmp_path, keep=True) as tmp_dir: 37 | cc_json = tmp_dir / "cookiecutter.json" 38 | copy_template(tmp_dir) 39 | assert (cc_json).is_file() 40 | 41 | # check that field names are dumped by alias 42 | with open(cc_json, "r") as f: 43 | jsondata = json.load(f) 44 | assert "_copy_without_render" in jsondata 45 | --------------------------------------------------------------------------------