├── .coveragerc ├── .devcontainer ├── Dockerfile ├── devcontainer.json ├── docker-compose.devcontainer.build.yml └── scripts │ └── custom_cli.sh ├── .dockerignore ├── .env.example ├── .flake8 ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── checks.yml │ └── packages_update.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── config_example.yaml ├── docker-compose.yml ├── imgs ├── badges │ ├── coverage.svg │ ├── flake.svg │ └── tests.svg └── pyplate_logo_v2.png ├── logs └── .gitkeep ├── mypy.ini ├── pyproject.toml ├── pytest.ini ├── requirements ├── README.md ├── base.txt ├── dev.txt └── prod.txt ├── scripts ├── fix_checks.sh ├── run_dev.sh ├── setup_project.sh └── update_packages.sh ├── setup.py └── src ├── __init__.py ├── loaders ├── __init__.py └── config_loader.py ├── main.py ├── models └── __init__.py ├── notebook.ipynb ├── settings ├── README.md ├── __init__.py ├── base.py └── components │ ├── __init__.py │ └── logging.py ├── tests ├── .env.test ├── __init__.py ├── conftest.py ├── test_loaders │ ├── __init__.py │ └── test_config_loader.py ├── test_main.py ├── test_settings │ ├── __init__.py │ └── test_base.py └── test_utils │ ├── __init__.py │ └── test_yaml_reader.py └── utils ├── __init__.py └── yaml_reader.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *__init__*, 4 | *test*, 5 | */usr/*, 6 | main.py 7 | 8 | # main.py should be tested if there is a proper logic in there (currently it's a "placeholder") 9 | 10 | [report] 11 | precision=2 12 | fail_under=100.00 13 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-rc 2 | 3 | RUN apt-get update 4 | 5 | WORKDIR /workspace 6 | 7 | # * install needed libs 8 | COPY requirements/ requirements/ 9 | RUN pip install --upgrade pip 10 | RUN pip3 install -r requirements/dev.txt 11 | 12 | # * modify bashrc to setup custom cli 13 | COPY .devcontainer/scripts/custom_cli.sh .devcontainer/scripts/custom_cli.sh 14 | RUN cat .devcontainer/scripts/custom_cli.sh >> ~/.bashrc 15 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaceFolder": "/workspace", 3 | "dockerComposeFile": "docker-compose.devcontainer.build.yml", 4 | "service": "app-name", 5 | "customizations": { 6 | "vscode": { 7 | "settings": { 8 | "jupyter.notebookFileRoot": "${workspaceFolder}", 9 | "python.testing.pytestArgs": [ 10 | "." 11 | ], 12 | "python.testing.unittestEnabled": false, 13 | "python.testing.pytestEnabled": true, 14 | "editor.rulers": [ 15 | { 16 | "column": 120, 17 | "color": "#5b5858" 18 | } 19 | ] 20 | }, 21 | "extensions": [ 22 | // python related 23 | "ms-python.python", 24 | "ms-python.vscode-pylance", 25 | "ms-python.black-formatter", 26 | "ms-python.isort", 27 | 28 | // formatting / highlighting 29 | "aaron-bond.better-comments", 30 | "mechatroner.rainbow-csv", 31 | "redhat.vscode-yaml", 32 | "esbenp.prettier-vscode", 33 | 34 | // tools 35 | "ms-toolsai.jupyter", 36 | "ms-mssql.mssql", 37 | "ms-azuretools.vscode-docker", 38 | "eamodio.gitlens", 39 | "yzhang.markdown-all-in-one", 40 | "GitHub.vscode-github-actions" 41 | ] 42 | } 43 | }, 44 | "postStartCommand": "cp -n .env.example .env && git config --global --add safe.directory /workspace", 45 | "overrideCommand": true 46 | } 47 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.devcontainer.build.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app-name: 5 | volumes: 6 | - ..:/workspace 7 | build: 8 | context: .. 9 | dockerfile: .devcontainer/Dockerfile 10 | tty: true # * will keep container running so we can execute commands on it -- this is needed for testing in pipeline 11 | container_name: app-name-container 12 | -------------------------------------------------------------------------------- /.devcontainer/scripts/custom_cli.sh: -------------------------------------------------------------------------------- 1 | 2 | # * Show git branch name and make a new line 3 | force_color_prompt=yes 4 | color_prompt=yes 5 | parse_git_branch() { 6 | git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/' 7 | } 8 | if [ "$color_prompt" = yes ]; then 9 | PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[01;31m\]$(parse_git_branch)\[\033[00m\]\n\$ ' 10 | else 11 | PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w$(parse_git_branch)\n\$ ' 12 | fi 13 | unset color_prompt force_color_prompt 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | src/tests/ 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ENVIRONMENT=dev 2 | DEBUG=True 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | per-file-ignores = __init__.py:F401 3 | max-line-length = 120 4 | exclude = venv/* 5 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How Can I Contribute? 2 | 3 | ## Reporting Bugs 4 | 5 | // TODO 6 | 7 | ## Suggesting Enhancements 8 | 9 | // TODO 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Branch naming convention 2 | 3 | - 4 | 5 | e.g.: 6 | 7 | * 69-add-login-endpoint 8 | * 420-fix-data-scraper 9 | 10 | 11 | ## Checklist before requesting a review 12 | - [ ] I have performed a self-review of my code 13 | - [ ] If it is a core feature, I have tested it 14 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: checks 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**/README.md' 7 | - 'scripts/*' 8 | - 'imgs/*' 9 | - 'LICENSE' 10 | - '.env.example' 11 | 12 | jobs: 13 | checks: 14 | runs-on: ubuntu-latest 15 | 16 | env: 17 | DEVCONTAINER_COMPOSE_PATH: ./.devcontainer/docker-compose.devcontainer.build.yml 18 | EXECUTE_IN_CONTAINER: docker exec app-name-container 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: build and run compose for testing 24 | run: docker-compose -f $DEVCONTAINER_COMPOSE_PATH up -d 25 | 26 | - name: check formatter (black) 27 | run: $EXECUTE_IN_CONTAINER black src/ --check --diff --color 28 | 29 | - name: check linter (flake8) 30 | run: $EXECUTE_IN_CONTAINER flake8 src/ --statistics --tee --output-file=flake8stats.txt 31 | 32 | - name: check typing (mypy) 33 | run: $EXECUTE_IN_CONTAINER mypy src/ 34 | 35 | - name: test with pytest 36 | run: $EXECUTE_IN_CONTAINER coverage run -m pytest ./src/ --junitxml=junit.xml 37 | 38 | - name: check coverage with coveragepy # also build an xml report for badge 39 | run: | 40 | $EXECUTE_IN_CONTAINER coverage report -m 41 | $EXECUTE_IN_CONTAINER coverage xml 42 | 43 | - name: create badges and add them to repo 44 | run: | 45 | $EXECUTE_IN_CONTAINER genbadge coverage --input-file=coverage.xml --output-file=imgs/badges/coverage.svg 46 | $EXECUTE_IN_CONTAINER genbadge tests --input-file=junit.xml --output-file=imgs/badges/tests.svg 47 | $EXECUTE_IN_CONTAINER genbadge flake8 --input-file=flake8stats.txt --output-file=imgs/badges/flake.svg 48 | 49 | git config --global user.name "github-actions[bot]" 50 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 51 | 52 | git add imgs/badges/ 53 | git commit -m "Update badges" || echo "No changes to commit" 54 | git push 55 | 56 | - name: Down compose 57 | run: docker-compose -f $DEVCONTAINER_COMPOSE_PATH down 58 | -------------------------------------------------------------------------------- /.github/workflows/packages_update.yml: -------------------------------------------------------------------------------- 1 | name: packages-update 2 | 3 | on: 4 | schedule: 5 | - cron: '0 20 * * *' 6 | 7 | jobs: 8 | createPullRequest: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up Python 3.12 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.12 18 | 19 | - name: Install package updater 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pur 23 | 24 | - name: Update packages 25 | run: ./scripts/update_packages.sh 26 | 27 | - name: Create Pull Request 28 | uses: peter-evans/create-pull-request@v5 29 | with: 30 | branch: auto-update-packages 31 | commit-message: Update packages 32 | title: '[AUTO] Update packages' 33 | body: > 34 | This PR is auto-generated by 35 | [create-pull-request](https://github.com/peter-evans/create-pull-request). 36 | labels: auto 37 | delete-branch: false 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | private/* 2 | config.yaml 3 | 4 | *.log 5 | 6 | # auto generated reports that should be excluded from repo 7 | flake8stats.txt 8 | junit.xml 9 | 10 | # coverage 11 | .coverage 12 | 13 | # vscode related settings - not needed because all settings should be declared in devcontainer 14 | .vscode 15 | 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | share/python-wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | MANIFEST 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .nox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *.cover 64 | *.py,cover 65 | .hypothesis/ 66 | .pytest_cache/ 67 | cover/ 68 | 69 | # Translations 70 | *.mo 71 | *.pot 72 | 73 | # Django stuff: 74 | *.log 75 | local_settings.py 76 | db.sqlite3 77 | db.sqlite3-journal 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | .pybuilder/ 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # IPython 97 | profile_default/ 98 | ipython_config.py 99 | 100 | # pyenv 101 | # For a library or package, you might want to ignore these files since the code is 102 | # intended to run in multiple environments; otherwise, check them in: 103 | # .python-version 104 | 105 | # pipenv 106 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 107 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 108 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 109 | # install all needed dependencies. 110 | #Pipfile.lock 111 | 112 | # poetry 113 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 114 | # This is especially recommended for binary packages to ensure reproducibility, and is more 115 | # commonly ignored for libraries. 116 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 117 | #poetry.lock 118 | 119 | # pdm 120 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 121 | #pdm.lock 122 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 123 | # in version control. 124 | # https://pdm.fming.dev/#use-with-ide 125 | .pdm.toml 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .venv 140 | env/ 141 | venv/ 142 | ENV/ 143 | env.bak/ 144 | venv.bak/ 145 | 146 | # Spyder project settings 147 | .spyderproject 148 | .spyproject 149 | 150 | # Rope project settings 151 | .ropeproject 152 | 153 | # mkdocs documentation 154 | /site 155 | 156 | # mypy 157 | .mypy_cache/ 158 | .dmypy.json 159 | dmypy.json 160 | 161 | # Pyre type checker 162 | .pyre/ 163 | 164 | # pytype static type analyzer 165 | .pytype/ 166 | 167 | # Cython debug symbols 168 | cython_debug/ 169 | 170 | # PyCharm 171 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 172 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 173 | # and can be added to the global gitignore or merged into this file. For a more nuclear 174 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 175 | #.idea/ 176 | -------------------------------------------------------------------------------- /.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 | 4 | # ! doesn't include pytest/coveragepy 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v3.2.0 8 | hooks: 9 | - id: trailing-whitespace # ? trims trailing whitespace 10 | exclude: (imgs|.devcontainer)/.* 11 | - id: end-of-file-fixer # ? ensures that a file is either empty, or ends with one newline 12 | exclude: (imgs|.devcontainer)/.* 13 | - id: check-yaml # ? checks yaml files for parseable syntax 14 | - id: check-added-large-files # ? check-added-large-files 15 | - id: check-toml # ? checks toml files for parseable syntax 16 | - id: requirements-txt-fixer # ? sorts entries in requirements.txt 17 | - id: detect-private-key # ? detects the presence of private keys 18 | 19 | - repo: https://github.com/psf/black 20 | rev: 23.9.1 21 | hooks: 22 | - id: black 23 | files: src/ 24 | 25 | - repo: https://github.com/pycqa/flake8 26 | rev: '6.1.0' 27 | hooks: 28 | - id: flake8 29 | files: src/ 30 | 31 | - repo: https://github.com/pre-commit/mirrors-mypy 32 | rev: 'v1.8.0' 33 | hooks: 34 | - id: mypy 35 | files: src/ 36 | exclude: ^src/tests/ 37 | additional_dependencies: [types-PyYAML==6.0.12.11] 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-rc 2 | 3 | RUN apt-get update 4 | 5 | WORKDIR /workspace 6 | 7 | # * install needed libs 8 | COPY requirements/ requirements/ 9 | RUN pip install --upgrade pip 10 | RUN pip3 install -r requirements/prod.txt 11 | 12 | COPY ./src ./ 13 | CMD ["python", "./main.py"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Jakub Tolsciuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Workflow status](https://github.com/workata/pyplate/actions/workflows/checks.yml/badge.svg) coverge-badge pytest-badge flake-badge pre-commit code style: black Contributions Welcome ![PyPlate stars](https://img.shields.io/github/stars/workata/pyplate) 13 | 14 | 15 |

16 | pyplate logo 17 |

18 | 19 | 20 | ## About the project 21 | Template for a standard (non-framework related) python project. The point of this repo is to have a basic project layout with working CI/CD, devcontainer and integrated common python tools. **It should be** later on **adjusted according to the needs** of specific project. 22 | 23 | ### Integrated tools 24 | 25 | - **[black](https://black.readthedocs.io/en/stable/)** (formatter) 26 | - **[flake8](https://flake8.pycqa.org/en/latest/)** (linter) 27 | - **[mypy](https://mypy.readthedocs.io/en/stable/)** (type checker) 28 | - **[pytest](https://docs.pytest.org/en/7.1.x/contents.html)** (unit tests) 29 | - **[pydantic](https://docs.pydantic.dev/latest/)** (data validation, models) 30 | - **[pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)** (settings management) 31 | - **[pur](https://github.com/alanhamlett/pip-update-requirements)** (package updater) 32 | - **[pre-commit](https://pre-commit.com/)** (git hooks, running checks) 33 | - **[devcontainer](https://code.visualstudio.com/docs/devcontainers/containers)** (development inside container) 34 | 35 | 36 | ## Setup 37 | 38 | #### a) Setup with devcontainer (recommended) 39 | This template project uses devcontainer (VS code) to setup everything. So just follow [official documentation](https://code.visualstudio.com/docs/devcontainers/tutorial) to meet prerequisites. Then open this template project in container (using VS code) and you are ready to code! 40 | 41 | #### b) Setup without devcontainer 42 | 43 | Copy env file 44 | ```sh 45 | cp -n .env.example .env 46 | ``` 47 | 48 | Create new venv 49 | ```sh 50 | python3 -m venv ./venv 51 | ``` 52 | 53 | Activate venv 54 | ```sh 55 | . ./venv/bin/activate 56 | ``` 57 | 58 | Install needed requirements 59 | ```sh 60 | pip install -r requirements/dev.txt 61 | ``` 62 | 63 | Run unit tests to check if it works 64 | ```sh 65 | pytest ./src/ 66 | ``` 67 | 68 | --- 69 | 70 | #### Run setup script 71 | 72 | Run one-time setup script to do some adjustments 73 | ```sh 74 | ./scripts/setup_project.sh 75 | ``` 76 | 77 | If you are not using devcontainer or unix-like OS this script will not work so just do the following: 78 | 79 | - [ ] Find and replace all occurrences of 'app-name' to your project name phrase. 80 | - [ ] Open `setup.py` and update information about the project like Author, Description etc. 81 | - [ ] Adjust (remove/add) tools and related configs according to your needs 82 | 83 | #### *Optionally add pre-commit git hook* 84 | [Pre-commit](https://pre-commit.com/) configuration is enabled for this project. To add the hook run the following command: 85 | 86 | ```sh 87 | pre-commit install 88 | ``` 89 | 90 | ## Test code 91 | 92 | On every commit code should be static tested/checked/formatted automatically (using [pre-commit](https://pre-commit.com/) tool). If you want to run static tests + unit tests then run: 93 | 94 | ```sh 95 | ./scripts/run_tests.sh 96 | ``` 97 | 98 | ## Update packages 99 | You can manually run a script that will check for new versions of packages which are used in this project. It will update both requirements files (`base.txt`, `dev.txt`). 100 | 101 | ```sh 102 | ./scripts/update_packages.sh 103 | ``` 104 | 105 | On top of that there is a workflow added (`packages_update.yml`) that will create new Pull Request automatically with updated packages. Cronjob for this task is set for: `0 20 * * *` (every day - 20:00). 106 | 107 | ## Dockerize 108 | 109 | There are two Dockerfiles in this project. 110 | 111 | - `.devcontainer/Dockerfile` 112 | - `Dockerfile` 113 | 114 | First is for developing in a devcontainer and it's also used for running stuff in the pipeline. The second is for deploying the product as shown below. You should exclude unnecessary files (such as tests) using `.dockerignore` to keep the "production ready" container as ligthweight as possible. 115 | 116 | Build image 117 | ```sh 118 | docker build . --tag app-image 119 | ``` 120 | 121 | Create container and run it 122 | ```sh 123 | docker run app-image 124 | ``` 125 | -------------------------------------------------------------------------------- /config_example.yaml: -------------------------------------------------------------------------------- 1 | instance_file_path: ./instances/example1.txt 2 | conf: 3 | name: a 4 | example_list_1: [a, b, c] 5 | example_list_2: 6 | - a 7 | - b 8 | - c 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app-name-service: 5 | container_name: app-name-container 6 | build: ./ 7 | env_file: 8 | - .env 9 | -------------------------------------------------------------------------------- /imgs/badges/coverage.svg: -------------------------------------------------------------------------------- 1 | coverage: 100.00%coverage100.00% -------------------------------------------------------------------------------- /imgs/badges/flake.svg: -------------------------------------------------------------------------------- 1 | flake8: 0 C, 0 W, 0 Iflake80 C, 0 W, 0 I -------------------------------------------------------------------------------- /imgs/badges/tests.svg: -------------------------------------------------------------------------------- 1 | tests: 6tests6 -------------------------------------------------------------------------------- /imgs/pyplate_logo_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/imgs/pyplate_logo_v2.png -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/logs/.gitkeep -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = true 3 | check_untyped_defs = True 4 | disallow_untyped_defs = True 5 | disallow_incomplete_defs = True 6 | disallow_untyped_decorators = False 7 | disallow_any_unimported = False 8 | warn_return_any = True 9 | warn_unused_ignores = True 10 | no_implicit_optional = True 11 | show_error_codes = True 12 | 13 | exclude = src/tests/ 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = ./src/ 3 | -------------------------------------------------------------------------------- /requirements/README.md: -------------------------------------------------------------------------------- 1 | # App requirements 2 | 3 | Here you should have your all python requirments (libs) listed. It would be good to set precise version of specific libs (or at least version range). On top of that those requirements should be divided based on environment. For example you don't need testing libs (like pytest) on production env. 4 | 5 | Example requirements structure: 6 | ``` 7 | ├── requirements 8 | ├── base.txt 9 | ├── dev.txt (for developing/testing; includes base) 10 | └── prod.txt (for prod container; includes base) 11 | ``` 12 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | jupyter==1.0.0 2 | pydantic==2.7.1 3 | pydantic-settings==2.2.1 4 | python-dotenv==1.0.1 5 | PyYAML==6.0.1 6 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | assertpy==1.1 3 | black[jupyter]==24.4.2 4 | coverage==7.5.1 5 | flake8==7.0.0 6 | genbadge[all]==1.1.1 7 | mypy==1.10.0 8 | pre-commit==3.7.0 9 | pur==7.3.1 10 | 11 | pytest==8.2.0 12 | types-PyYAML==6.0.12.20240311 13 | -------------------------------------------------------------------------------- /requirements/prod.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | # most likely you will only need base libs here but sometimes it may be 4 | # necessary to include for example lib related with deployment (like pyngrok) 5 | -------------------------------------------------------------------------------- /scripts/fix_checks.sh: -------------------------------------------------------------------------------- 1 | # run checks and potentialy fix them 2 | 3 | echo "Check typing with mypy..." 4 | mypy ./src/ 5 | 6 | echo "Format with Black..." 7 | black ./src/ 8 | 9 | echo "Lint with flake8..." 10 | flake8 ./src/ 11 | 12 | echo "Test with PyTest... (+ check coverage)" 13 | coverage run -m pytest ./src/ 14 | 15 | echo "Report coverage..." 16 | coverage report -m 17 | -------------------------------------------------------------------------------- /scripts/run_dev.sh: -------------------------------------------------------------------------------- 1 | python3 ./src/main.py 2 | -------------------------------------------------------------------------------- /scripts/setup_project.sh: -------------------------------------------------------------------------------- 1 | unset project_name 2 | 3 | # * read user input 4 | while read -p "[Required] Enter project name (without whitespaces): " project_name; do 5 | if [[ -z "${project_name}" ]]; then # check if its empty value 6 | echo "Project name cannot be empty!" 7 | else 8 | echo "Creating ${project_name}..." 9 | break 10 | fi 11 | done 12 | read -p "[Optional] Enter author: " author 13 | read -p "[Optional] Enter short description: " description 14 | 15 | # * clear README file 16 | echo "# ${project_name}" > README.md 17 | 18 | # * replace app-name with project name 19 | find . -type f -not \( \ 20 | -path "./venv/*" -o \ 21 | -path "./.git/*" -o \ 22 | -path "./imgs/*" -o \ 23 | -path "./scripts/*" -o \ 24 | -path "./logs/*" -o \ 25 | -path "./requirements/*" \ 26 | \) -exec sed -i "s/app-name/${project_name}/g" {} \; 27 | 28 | # * replace author/desciption in setup.py 29 | find . -type f -path "./setup.py" -exec sed -i "s/__author__/${author}/g" {} \; 30 | find . -type f -path "./setup.py" -exec sed -i "s/__description__/${description}/g" {} \; 31 | 32 | # * remove unnecessary files 33 | rm -rf imgs/* 34 | rm scripts/setup_project.sh 35 | 36 | echo "Setup is done!" 37 | -------------------------------------------------------------------------------- /scripts/update_packages.sh: -------------------------------------------------------------------------------- 1 | echo "Updating packages in requirements/base.txt file..." 2 | pur -r requirements/base.txt 3 | echo "Updating packages in requirements/dev.txt file..." 4 | pur -r requirements/dev.txt 5 | echo "Commit your changes only if all tests are passing!" 6 | echo "Verify with: pytest ./src/" 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='app-name', 5 | version='0.0.1', 6 | author='__author__', 7 | description='__description__', 8 | author_email='author@gmail.com', 9 | url='www.project-url.com', 10 | keywords='template, example', 11 | python_requires='>=3.12, <4', 12 | classifiers=[ 13 | 'Programming Language :: Python :: 3', 14 | 'License :: OSI Approved :: MIT License', 15 | 'Operating System :: OS Independent' 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/src/__init__.py -------------------------------------------------------------------------------- /src/loaders/__init__.py: -------------------------------------------------------------------------------- 1 | from .config_loader import ConfigLoader 2 | -------------------------------------------------------------------------------- /src/loaders/config_loader.py: -------------------------------------------------------------------------------- 1 | from utils import YamlReader 2 | from typing import Any 3 | 4 | 5 | class ConfigLoader: 6 | @classmethod 7 | def load(cls, config_file_path: str) -> Any: 8 | return YamlReader.read(file_path=config_file_path) 9 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | def main() -> None: 2 | print("Hello python!") 3 | 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /src/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/src/models/__init__.py -------------------------------------------------------------------------------- /src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "## Python project template overview" 9 | ] 10 | }, 11 | { 12 | "attachments": {}, 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "#### Access pydantic settings from .env file" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 14, 22 | "metadata": {}, 23 | "outputs": [ 24 | { 25 | "name": "stdout", 26 | "output_type": "stream", 27 | "text": [ 28 | "Env: dev, type: \n", 29 | "debug: True, type: \n" 30 | ] 31 | } 32 | ], 33 | "source": [ 34 | "from settings import get_settings\n", 35 | "\n", 36 | "\n", 37 | "settings = get_settings()\n", 38 | "\n", 39 | "print(f\"Env: {settings.environment}, type: {type(settings.environment)}\")\n", 40 | "print(f\"debug: {settings.debug}, type: {type(settings.debug)}\")" 41 | ] 42 | }, 43 | { 44 | "attachments": {}, 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "#### Use built in python logging with custom settings" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 16, 54 | "metadata": {}, 55 | "outputs": [ 56 | { 57 | "name": "stderr", 58 | "output_type": "stream", 59 | "text": [ 60 | "[INFO] log something\n" 61 | ] 62 | } 63 | ], 64 | "source": [ 65 | "import logging\n", 66 | "\n", 67 | "logging.config.dictConfig(settings.logging)\n", 68 | "\n", 69 | "logger = logging.getLogger(\"general\")\n", 70 | "logger.info(\"log something\")" 71 | ] 72 | }, 73 | { 74 | "attachments": {}, 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "#### Use config loader to load simple yaml configuration" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 17, 84 | "metadata": {}, 85 | "outputs": [ 86 | { 87 | "name": "stdout", 88 | "output_type": "stream", 89 | "text": [ 90 | "{'instance_file_path': './instances/example1.txt', 'conf': {'name': 'a', 'example_list_1': ['a', 'b', 'c'], 'example_list_2': ['a', 'b', 'c']}}\n" 91 | ] 92 | } 93 | ], 94 | "source": [ 95 | "from loaders import ConfigLoader\n", 96 | "\n", 97 | "\n", 98 | "config = ConfigLoader.load(config_file_path=\"./config_example.yaml\")\n", 99 | "print(config)" 100 | ] 101 | }, 102 | { 103 | "attachments": {}, 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "#### Working dir is set for project root" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 18, 113 | "metadata": {}, 114 | "outputs": [ 115 | { 116 | "name": "stdout", 117 | "output_type": "stream", 118 | "text": [ 119 | "/home/workata/projects/pyplate\n" 120 | ] 121 | } 122 | ], 123 | "source": [ 124 | "import os\n", 125 | "\n", 126 | "\n", 127 | "# * get the current working directory\n", 128 | "directory = os.getcwd()\n", 129 | "print(directory)" 130 | ] 131 | } 132 | ], 133 | "metadata": { 134 | "kernelspec": { 135 | "display_name": "Python 3", 136 | "language": "python", 137 | "name": "python3" 138 | }, 139 | "language_info": { 140 | "codemirror_mode": { 141 | "name": "ipython", 142 | "version": 3 143 | }, 144 | "file_extension": ".py", 145 | "mimetype": "text/x-python", 146 | "name": "python", 147 | "nbconvert_exporter": "python", 148 | "pygments_lexer": "ipython3", 149 | "version": "3.10.6" 150 | }, 151 | "orig_nbformat": 4, 152 | "vscode": { 153 | "interpreter": { 154 | "hash": "949777d72b0d2535278d3dc13498b2535136f6dfe0678499012e853ee9abcab1" 155 | } 156 | } 157 | }, 158 | "nbformat": 4, 159 | "nbformat_minor": 2 160 | } 161 | -------------------------------------------------------------------------------- /src/settings/README.md: -------------------------------------------------------------------------------- 1 | ### App settings 2 | 3 | Default settings should be located in `base.py` file. On top of that you may want to include additional settings for different environments. These additional settings should inherit from `Settings` class. 4 | 5 | Example settings structure: 6 | ``` 7 | ├── src 8 | └── settings 9 | ├── components 10 | | ├── __init__.py 11 | | └── logging.py 12 | ├── __init__.py 13 | ├── base.py 14 | ├── dev.py 15 | ├── staging.py 16 | └── prod.py 17 | ``` 18 | -------------------------------------------------------------------------------- /src/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import get_settings 2 | -------------------------------------------------------------------------------- /src/settings/base.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from typing import Any, Dict 3 | 4 | from pydantic_settings import BaseSettings, SettingsConfigDict 5 | 6 | from .components.logging import logging_settings 7 | 8 | 9 | class Settings(BaseSettings): 10 | environment: str = "dev" 11 | debug: bool = False 12 | logging: Dict[str, Any] = logging_settings 13 | 14 | model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") 15 | 16 | 17 | @lru_cache 18 | def get_settings() -> BaseSettings: 19 | return Settings() 20 | -------------------------------------------------------------------------------- /src/settings/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/src/settings/components/__init__.py -------------------------------------------------------------------------------- /src/settings/components/logging.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | logging_settings = { 3 | "version": 1, 4 | "disable_existing_loggers": False, 5 | "formatters": { 6 | "verbose": { 7 | "format": "[{levelname}][{asctime}] {message}", 8 | "style": "{", 9 | }, 10 | "simple": { 11 | "format": "[{levelname}] {message}", 12 | "style": "{", 13 | }, 14 | }, 15 | "handlers": { 16 | "console": { 17 | "class": "logging.StreamHandler", 18 | "formatter": "simple" 19 | }, 20 | "file": { 21 | "class": "logging.FileHandler", 22 | "filename": "./logs/all.log", 23 | "formatter": "verbose" 24 | }, 25 | }, 26 | "loggers": { 27 | "general": { 28 | "handlers": ["console", "file"], 29 | "level": "INFO", 30 | } 31 | }, 32 | } 33 | # fmt: on 34 | -------------------------------------------------------------------------------- /src/tests/.env.test: -------------------------------------------------------------------------------- 1 | # * env vars for testing only 2 | ENVIRONMENT=test 3 | DEBUG=True 4 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from dotenv import find_dotenv 3 | 4 | from settings.base import Settings 5 | 6 | 7 | @pytest.fixture 8 | def settings(): 9 | """ 10 | Overwrite config source for testing 11 | https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support 12 | """ 13 | return Settings(_env_file=find_dotenv(".env.test")) 14 | -------------------------------------------------------------------------------- /src/tests/test_loaders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/src/tests/test_loaders/__init__.py -------------------------------------------------------------------------------- /src/tests/test_loaders/test_config_loader.py: -------------------------------------------------------------------------------- 1 | from loaders import ConfigLoader 2 | import pytest 3 | from assertpy import assert_that 4 | from typing import Any 5 | 6 | 7 | @pytest.fixture 8 | def loaded_config_file() -> Any: 9 | return ConfigLoader.load(config_file_path="./config_example.yaml") 10 | 11 | 12 | def test_config_example_content(loaded_config_file: Any) -> None: 13 | assert_that(loaded_config_file["instance_file_path"]).is_equal_to("./instances/example1.txt") 14 | -------------------------------------------------------------------------------- /src/tests/test_main.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, MagicMock 2 | from main import main 3 | 4 | 5 | @patch("builtins.print") 6 | def test_main_logic(mock_print: MagicMock) -> None: 7 | main() 8 | mock_print.assert_any_call("Hello python!") 9 | -------------------------------------------------------------------------------- /src/tests/test_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/src/tests/test_settings/__init__.py -------------------------------------------------------------------------------- /src/tests/test_settings/test_base.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from assertpy import assert_that 4 | 5 | from settings import get_settings 6 | from settings.components.logging import logging_settings 7 | 8 | 9 | def test_settings_values(settings): 10 | assert_that(settings.environment).is_equal_to("test") 11 | assert_that(settings.environment).is_type_of(str) 12 | 13 | assert_that(settings.debug).is_equal_to(True) 14 | assert_that(settings.debug).is_type_of(bool) 15 | 16 | assert_that(settings.logging).is_equal_to(logging_settings) 17 | assert_that(settings.logging).is_type_of(dict) 18 | 19 | 20 | @mock.patch("settings.base.Settings") 21 | def test_get_settings_should_create_settings(mock_settings_cls): 22 | settings = get_settings() 23 | mock_settings_cls.assert_called_once_with() 24 | assert_that(settings).is_equal_to(mock_settings_cls.return_value) 25 | -------------------------------------------------------------------------------- /src/tests/test_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workata/pyplate/706d4932d18f6f2675399ec5de97996f7fb8fac3/src/tests/test_utils/__init__.py -------------------------------------------------------------------------------- /src/tests/test_utils/test_yaml_reader.py: -------------------------------------------------------------------------------- 1 | from utils import YamlReader 2 | from unittest.mock import patch, mock_open, Mock, MagicMock 3 | from assertpy import assert_that 4 | import yaml 5 | import pytest 6 | 7 | 8 | def test_read_correct_yaml() -> None: 9 | with patch("builtins.open", mock_open(read_data="a: b\nc: d")) as _: 10 | content = YamlReader.read(file_path=Mock()) 11 | assert_that(content).is_equal_to({"a": "b", "c": "d"}) 12 | 13 | 14 | @patch.object(yaml, "safe_load") 15 | def test_read_incorrect_yaml(mock_safe_load: MagicMock) -> None: 16 | mock_safe_load.side_effect = yaml.YAMLError 17 | with patch("builtins.open", mock_open(read_data=None)) as _: 18 | with pytest.raises(yaml.YAMLError) as _: 19 | YamlReader.read(file_path=Mock()) 20 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .yaml_reader import YamlReader 2 | -------------------------------------------------------------------------------- /src/utils/yaml_reader.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from typing import Any 3 | 4 | 5 | class YamlReader: 6 | @classmethod 7 | def read(cls, file_path: str) -> Any: 8 | with open(file_path, "r") as file: 9 | try: 10 | return yaml.safe_load(file) 11 | except yaml.YAMLError as exc: 12 | print(exc) 13 | raise 14 | --------------------------------------------------------------------------------