├── .codespell-ignore-words.txt ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .tmuxp.yml ├── LICENSE ├── Makefile ├── README.md ├── README.rst ├── docker-compose.yml ├── docs ├── _static │ ├── coverage.svg │ ├── dephell.svg │ ├── example_config.yml │ ├── example_docker_compose.yml │ ├── example_dockerfile │ ├── exampleservice │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── config.yml │ │ │ ├── endpoints │ │ │ │ ├── __init__.py │ │ │ │ └── v1 │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── errors.py │ │ │ │ │ ├── example.py │ │ │ │ │ └── models.py │ │ │ └── main.py │ │ ├── pyproject.toml │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── service_test.py │ └── logo.png ├── api │ ├── fastapi_serviceutils.app.endpoints.default.rst │ ├── fastapi_serviceutils.app.endpoints.rst │ ├── fastapi_serviceutils.app.handlers.rst │ ├── fastapi_serviceutils.app.middlewares.rst │ ├── fastapi_serviceutils.app.rst │ ├── fastapi_serviceutils.cli.rst │ ├── fastapi_serviceutils.rst │ ├── fastapi_serviceutils.utils.docs.rst │ ├── fastapi_serviceutils.utils.external_resources.rst │ ├── fastapi_serviceutils.utils.rst │ ├── fastapi_serviceutils.utils.tests.rst │ └── modules.rst ├── conf.py ├── create_service.rst ├── create_service_help.txt ├── dependency_management.rst ├── deployment.rst ├── deployment_basics.rst ├── development.rst ├── doc_requirements.txt ├── documentation.rst ├── exampleservice.rst ├── external_resources.rst ├── external_resources_databases.rst ├── external_resources_services.rst ├── getting_started.rst ├── helpers.rst ├── index.rst ├── makefile.rst ├── makefile_help.txt ├── see_also.rst ├── sources.rst ├── testing.rst ├── tmuxp.rst └── usage.rst ├── fastapi_serviceutils ├── __init__.py ├── app │ ├── __init__.py │ ├── endpoints │ │ ├── __init__.py │ │ └── default │ │ │ ├── __init__.py │ │ │ ├── alive.py │ │ │ ├── config.py │ │ │ └── models.py │ ├── handlers │ │ └── __init__.py │ ├── logger.py │ ├── middlewares │ │ └── __init__.py │ └── service_config.py ├── cli │ ├── __init__.py │ └── create_service.py └── utils │ ├── __init__.py │ ├── docs │ ├── __init__.py │ └── apidoc.py │ ├── external_resources │ ├── __init__.py │ ├── dbs.py │ └── services.py │ └── tests │ ├── __init__.py │ └── endpoints.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── base_service_config_test.py ├── configs ├── config.yml ├── config2.yml └── config3.yml ├── create_service_test.py ├── default_endpoints_test.py ├── endpoints_test.py ├── external_resources_db_test.py ├── external_resources_external_service_test.py └── invalid_configs ├── invalid_config.yml ├── invalid_config10.yml ├── invalid_config11.yml ├── invalid_config12.yml ├── invalid_config13.yml ├── invalid_config14.yml ├── invalid_config15.yml ├── invalid_config16.yml ├── invalid_config17.yml ├── invalid_config18.yml ├── invalid_config19.yml ├── invalid_config2.yml ├── invalid_config20.yml ├── invalid_config3.yml ├── invalid_config4.yml ├── invalid_config5.yml ├── invalid_config6.yml ├── invalid_config7.yml ├── invalid_config8.yml └── invalid_config9.yml /.codespell-ignore-words.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skallfass/fastapi_serviceutils/8033450fc556396c735fb7c783ebe36e28a60ac0/.codespell-ignore-words.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | docs/_build 6 | htmlcov/ 7 | .coverage 8 | .coverage.* 9 | .pytest_cache/ 10 | dist/ 11 | *.egg-info/ 12 | log 13 | -------------------------------------------------------------------------------- /.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 | #fail_fast: True 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v2.3.0 7 | hooks: 8 | - id: check-docstring-first 9 | types: [python] 10 | - id: trailing-whitespace 11 | args: [--markdown-linebreak-ext=md] 12 | types: [python] 13 | - id: double-quote-string-fixer 14 | types: [python] 15 | - id: check-executables-have-shebangs 16 | - id: check-case-conflict 17 | types: [python] 18 | - id: check-toml 19 | - id: check-yaml 20 | - repo: https://gitlab.com/smop/pre-commit-hooks 21 | rev: v1.0.0 22 | hooks: 23 | - id: check-poetry 24 | - id: check-gitlab-ci 25 | - repo: https://github.com/pre-commit/pygrep-hooks 26 | rev: v1.4.1 27 | hooks: 28 | - id: python-use-type-annotations 29 | types: [python] 30 | - id: python-no-log-warn 31 | types: [python] 32 | - repo: https://github.com/asottile/reorder_python_imports 33 | rev: v1.6.1 34 | hooks: 35 | - id: reorder-python-imports 36 | types: [python] 37 | - repo: https://github.com/pre-commit/pre-commit-hooks 38 | rev: v2.3.0 39 | hooks: 40 | - id: check-ast 41 | types: [python] 42 | - repo: https://github.com/pycqa/pydocstyle 43 | rev: 4.0.0 44 | hooks: 45 | - id: pydocstyle 46 | - repo: https://github.com/codespell-project/codespell 47 | rev: v1.16.0 48 | hooks: 49 | - id: codespell 50 | args: [--ignore-words=.codespell-ignore-words.txt] 51 | - repo: https://github.com/pre-commit/mirrors-yapf 52 | rev: v0.28.0 53 | hooks: 54 | - id: yapf 55 | types: [python] 56 | always_run: true 57 | - repo: https://gitlab.com/pycqa/flake8 58 | rev: 3.7.8 59 | hooks: 60 | - id: flake8 61 | types: [python] 62 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.7.4 2 | -------------------------------------------------------------------------------- /.tmuxp.yml: -------------------------------------------------------------------------------- 1 | session_name: fastapi_serviceutils 2 | start_directory: "${PWD}" 3 | shell_command_before: source "$VIRTUAL_ENV/bin/activate" 4 | environment: 5 | PYTHONPATH: ${PWD} 6 | SHELL: /usr/bin/zsh 7 | EDITOR: /opt/nvim/nvim.appimage 8 | windows: 9 | - window_name: development 10 | layout: main-vertical 11 | options: 12 | main-pane-width: 100 13 | panes: 14 | - focus: true 15 | - shell_command: 16 | - make update 17 | - shell_command: 18 | - pytest --looponfail 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Simon Kallfass 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | SHELL := zsh 3 | 4 | .PHONY : check clean docker docs finalize info init tests update help 5 | .SILENT: check clean docker docs finalize info init tests update _create_env _update_env 6 | 7 | python_meant=$(cat .python-version) 8 | python_used=$(which python) 9 | python_version=$(python --version) 10 | project_version=$(grep 'version = ' pyproject.toml | sed 's/version = //' | sed 's/"//' | sed 's/"//') 11 | 12 | 13 | # Show info about current project. 14 | info: 15 | echo "Project:" 16 | echo "========" 17 | echo "version: $(value project_version)" 18 | echo "" 19 | echo 'Python:' 20 | echo '=======' 21 | echo "used python: $(value python_used)" 22 | echo "version: $(value python_version)" 23 | echo "" 24 | echo "Info about current environment:" 25 | echo '===============================' 26 | poetry show 27 | 28 | # Clean the working directory from temporary files and caches. 29 | clean: 30 | rm -rf htmlcov; \ 31 | rm -rf *.egg-info; \ 32 | rm -rf dist; \ 33 | rm -rf **/__pycache__; \ 34 | rm -rf docs/_build; \ 35 | rm -rf .pytest_cache; \ 36 | rm -rf .coverage; \ 37 | rm -rf log; \ 38 | rm -rf pip-wheel-metadata 39 | 40 | _create_env: 41 | poetry install 42 | 43 | _update_env: 44 | poetry update 45 | 46 | _coverage_badge: 47 | coverage-badge -f -o docs/_static/coverage.svg 48 | 49 | _lock_it: 50 | poetry lock 51 | dephell deps convert --to requirements.txt 52 | cp requirements.txt docs/doc_requirements.txt 53 | 54 | _makefile_doc: 55 | make help > docs/makefile_help.txt 56 | 57 | _extract_docstrings: 58 | sphinx-apidoc -o docs/api --force --implicit-namespaces --module-first fastapi_serviceutils 59 | 60 | _html_documentation: 61 | PYTHONPATH=. sphinx-build -b html docs docs/_build 62 | 63 | _servicetools_doc: 64 | poetry run create_service --help > docs/create_service_help.txt && \ 65 | 66 | # Run tests using pytest. 67 | tests: 68 | docker-compose down; docker-compose up -d; sleep 2; pytest tests; docker-compose down 69 | 70 | # Finalize the main env. 71 | finalize: tests _lock_it 72 | 73 | # Create sphinx documentation for the project. 74 | docs: tests _coverage_badge _makefile_doc _servicetools_doc _extract_docstrings _html_documentation _lock_it 75 | 76 | doc: _makefile_doc _servicetools_doc _extract_docstrings _html_documentation 77 | 78 | # Initialize project 79 | init: _create_env _lock_it 80 | 81 | # Update environments based on pyproject.toml definitions. 82 | update: _update_env _lock_it 83 | 84 | # Run all checks defined in .pre-commit-config.yaml. 85 | check: 86 | pre-commit run --all-files 87 | 88 | # Show the help prompt. 89 | help: 90 | @ echo 'Helpers for development of fastapi_serviceutils.' 91 | @ echo 92 | @ echo ' Usage:' 93 | @ echo '' 94 | @ echo ' make [flags...]' 95 | @ echo '' 96 | @ echo ' Targets:' 97 | @ echo '' 98 | @ awk '/^#/{ comment = substr($$0,3) } comment && /^[a-zA-Z][a-zA-Z0-9_-]+ ?:/{ print " ", $$1, comment }' $(MAKEFILE_LIST) | column -t -s ':' | sort 99 | @ echo '' 100 | @ echo ' Flags:' 101 | @ echo '' 102 | @ awk '/^#/{ comment = substr($$0,3) } comment && /^[a-zA-Z][a-zA-Z0-9_-]+ ?\?=/{ print " ", $$1, $$2, comment }' $(MAKEFILE_LIST) | column -t -s '?=' | sort 103 | @ echo '' 104 | @ echo '' 105 | @ echo ' Note:' 106 | @ echo ' This workflow requires the following programs / tools to be installed:' 107 | @ echo ' - poetry' 108 | @ echo ' - dephell' 109 | @ echo ' - pyenv' 110 | 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![coverage](https://github.com/skallfass/fastapi_serviceutils/blob/master/docs/_static/coverage.svg) 2 | [![PyPI version fury.io](https://badge.fury.io/py/fastapi-serviceutils.svg)](https://pypi.python.org/pypi/fastapi-serviceutils/) 3 | [![PyPI pyversions](https://img.shields.io/pypi/pyversions/fastapi-serviceutils.svg)](https://pypi.python.org/pypi/fastapi-serviceutils/) 4 | [![Documentation Status](https://readthedocs.org/projects/fastapi-serviceutils/badge/?version=latest)](http://fastapi-serviceutils.readthedocs.io/?badge=latest) 5 | ![MIT License](https://img.shields.io/badge/License-MIT-blue.svg) 6 | ![Powered by Dephell](https://github.com/dephell/dephell/blob/master/assets/badge.svg) 7 | 8 | 9 | ## Installation 10 | 11 | ```bash 12 | pip install fastapi-serviceutils 13 | ``` 14 | 15 | 16 | ## Usage 17 | 18 | For more details and usage see: [readthedocs](https://fastapi-serviceutils.readthedocs.io/en/latest/) 19 | 20 | 21 | ## Development 22 | 23 | 24 | ### Getting started 25 | 26 | After cloning the repository initialize the development environment using: 27 | 28 | ```bash 29 | make init 30 | ``` 31 | 32 | This will create the dev environment exampleservice/dev. Activate it using: 33 | ```bash 34 | poetry shell 35 | ``` 36 | 37 | **Note:** 38 | 39 | Make sure to always activate the environment when you start working on the 40 | project in a new terminal using 41 | ```bash 42 | poetry shell 43 | ``` 44 | 45 | **ATTENTION:** the environment should also be activated before using ``make``. 46 | 47 | 48 | ### Updating dependencies 49 | 50 | After each change in dependencies defined at `pyproject.toml` run the 51 | following to ensure the environment-definition and lock-file are up to date: 52 | ```bash 53 | make update 54 | ``` 55 | 56 | 57 | ### Checking with linters and checkers 58 | 59 | To run all pre-commit-hooks manually run: 60 | ```bash 61 | make check 62 | ``` 63 | 64 | 65 | ### Info about project-state 66 | 67 | To show summary about project run: 68 | ```bash 69 | make info 70 | ``` 71 | 72 | 73 | ### Documentation 74 | 75 | The project's developer documentation is written using Sphinx. 76 | 77 | The documentation sources can be found in the docs subdirectory. 78 | 79 | The API-documentation is auto-generated from the docstrings of modules, 80 | classes, and functions. 81 | We're using the Google docstring standard. 82 | 83 | To generate the documentation, run: 84 | ```bash 85 | make docs 86 | ``` 87 | 88 | The output for generated HTML files is in the `docs/_build` directory. 89 | 90 | 91 | ### Tests 92 | 93 | For testing we use `pytest`, for details see 94 | [Pytest Docs](http://doc.pytest.org/en/latest/). 95 | To run all tests: 96 | 97 | ```bash 98 | make tests 99 | ``` 100 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | .. image:: https://github.com/skallfass/fastapi_serviceutils/blob/master/docs/_static/coverage.svg 4 | :target: https://github.com/skallfass/fastapi_serviceutils/blob/master/docs/_static/coverage.svg 5 | :alt: coverage 6 | 7 | 8 | .. image:: https://badge.fury.io/py/fastapi-serviceutils.svg 9 | :target: https://pypi.python.org/pypi/fastapi-serviceutils/ 10 | :alt: PyPI version fury.io 11 | 12 | 13 | .. image:: https://img.shields.io/pypi/pyversions/fastapi-serviceutils.svg 14 | :target: https://pypi.python.org/pypi/fastapi-serviceutils/ 15 | :alt: PyPI pyversions 16 | 17 | 18 | .. image:: https://readthedocs.org/projects/fastapi-serviceutils/badge/?version=latest 19 | :target: http://fastapi-serviceutils.readthedocs.io/?badge=latest 20 | :alt: Documentation Status 21 | 22 | 23 | .. image:: https://img.shields.io/badge/License-MIT-blue.svg 24 | :target: https://img.shields.io/badge/License-MIT-blue.svg 25 | :alt: MIT License 26 | 27 | 28 | .. image:: https://github.com/dephell/dephell/blob/master/assets/badge.svg 29 | :target: https://github.com/dephell/dephell/blob/master/assets/badge.svg 30 | :alt: Powered by Dephell 31 | 32 | 33 | Installation 34 | ------------ 35 | 36 | .. code-block:: bash 37 | 38 | pip install fastapi-serviceutils 39 | 40 | Usage 41 | ----- 42 | 43 | For more details and usage see: `readthedocs `_ 44 | 45 | Development 46 | ----------- 47 | 48 | Getting started 49 | ^^^^^^^^^^^^^^^ 50 | 51 | After cloning the repository initialize the development environment using: 52 | 53 | .. code-block:: bash 54 | 55 | make init 56 | 57 | This will create the dev environment exampleservice/dev. Activate it using: 58 | 59 | .. code-block:: bash 60 | 61 | poetry shell 62 | 63 | **Note:** 64 | 65 | Make sure to always activate the environment when you start working on the 66 | project in a new terminal using 67 | 68 | .. code-block:: bash 69 | 70 | poetry shell 71 | 72 | **ATTENTION:** the environment should also be activated before using ``make``. 73 | 74 | Updating dependencies 75 | ^^^^^^^^^^^^^^^^^^^^^ 76 | 77 | After each change in dependencies defined at ``pyproject.toml`` run the 78 | following to ensure the environment-definition and lock-file are up to date: 79 | 80 | .. code-block:: bash 81 | 82 | make update 83 | 84 | Checking with linters and checkers 85 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | To run all pre-commit-hooks manually run: 88 | 89 | .. code-block:: bash 90 | 91 | make check 92 | 93 | Info about project-state 94 | ^^^^^^^^^^^^^^^^^^^^^^^^ 95 | 96 | To show summary about project run: 97 | 98 | .. code-block:: bash 99 | 100 | make info 101 | 102 | Documentation 103 | ^^^^^^^^^^^^^ 104 | 105 | The project's developer documentation is written using Sphinx. 106 | 107 | The documentation sources can be found in the docs subdirectory. 108 | 109 | The API-documentation is auto-generated from the docstrings of modules, 110 | classes, and functions. 111 | We're using the Google docstring standard. 112 | 113 | To generate the documentation, run: 114 | 115 | .. code-block:: bash 116 | 117 | make docs 118 | 119 | The output for generated HTML files is in the ``docs/_build`` directory. 120 | 121 | Tests 122 | ^^^^^ 123 | 124 | For testing we use ``pytest``\ , for details see 125 | `Pytest Docs `_. 126 | To run all tests: 127 | 128 | .. code-block:: bash 129 | 130 | make tests 131 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | testdb: 5 | image: postgres:11.2-alpine 6 | ports: 7 | - "50005:5432" 8 | environment: 9 | - POSTGRES_USER=postgres 10 | - POSTGRES_PASSWORD=1234 11 | - POSTGRES_DB=monitordb 12 | -------------------------------------------------------------------------------- /docs/_static/coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 91% 19 | 91% 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/_static/dephell.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 33 | 41 | 42 | 50 | 55 | 59 | 60 | 70 | 80 | 81 | 112 | 117 | 118 | 126 | 131 | 135 | 136 | 138 | 146 | 147 | 151 | 153 | 155 | 160 | 161 | 166 | 167 | 172 | 173 | 176 | 181 | 186 | 187 | 190 | 195 | 200 | 205 | 207 | 212 | 217 | 222 | 227 | 228 | 229 | 232 | 235 | 238 | 241 | 244 | 247 | 250 | 253 | 256 | 259 | 262 | 265 | 268 | 271 | 274 | 277 | 280 | 283 | 286 | 289 | 292 | 295 | 298 | 301 | 304 | 307 | 310 | 313 | 316 | 319 | 320 | -------------------------------------------------------------------------------- /docs/_static/example_config.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: 15 | testservice: 16 | url: http://something:someport 17 | servicetype: 'rest' 18 | databases: 19 | testdb: 20 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 21 | databasetype: 'postgres' 22 | other: null 23 | logger: 24 | path: './log/EXAMPLESERVICE' 25 | filename: 'service_{mode}.log' 26 | level: 'debug' 27 | rotation: '1 days' 28 | retention: '1 months' 29 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 30 | available_environment_variables: 31 | env_vars: 32 | - EXAMPLESERVICE_SERVICE__MODE 33 | - EXAMPLESERVICE_SERVICE__PORT 34 | - EXAMPLESERVICE_LOGGER__LEVEL 35 | - EXAMPLESERVICE_LOGGER__PATH 36 | - EXAMPLESERVICE_LOGGER__FILENAME 37 | - EXAMPLESERVICE_LOGGER__ROTATION 38 | - EXAMPLESERVICE_LOGGER__RETENTION 39 | - EXAMPLESERVICE_LOGGER__FORMAT 40 | external_resources_env_vars: [] 41 | rules_env_vars: [] 42 | -------------------------------------------------------------------------------- /docs/_static/example_docker_compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | : 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | image: 9 | ports: 10 | - ":80" 11 | environment: 12 | - _SERVICE__MODE="prod" 13 | - ... 14 | volumes: 15 | - type: bind 16 | source: 17 | target: 18 | -------------------------------------------------------------------------------- /docs/_static/example_dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 2 | 3 | COPY requirements.txt ./ 4 | 5 | RUN pip install -r requirements.txt 6 | 7 | COPY . /app 8 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/config.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: null 16 | other: null 17 | logger: 18 | path: './log/EXAMPLESERVICE' 19 | filename: 'service_{mode}.log' 20 | level: 'debug' 21 | rotation: '1 days' 22 | retention: '1 months' 23 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [id: {extra[request_id]}] - {message}" 24 | available_environment_variables: 25 | env_vars: 26 | - SERVICE__MODE 27 | - SERVICE__PORT 28 | - LOGGER__LEVEL 29 | - LOGGER__PATH 30 | - LOGGER__FILENAME 31 | - LOGGER__ROTATION 32 | - LOGGER__RETENTION 33 | - LOGGER__FORMAT 34 | external_resources_env_vars: 35 | - EXTERNAL_RESOURCES__API__URL 36 | - EXTERNAL_RESOURCES__API__SCHEMA 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | from app.endpoints.v1 import ENDPOINTS as v1 2 | 3 | from fastapi_serviceutils.app.endpoints import set_version_endpoints 4 | 5 | LATEST = set_version_endpoints( 6 | endpoints=v1, 7 | version='latest', 8 | prefix_template='{route}' 9 | ) 10 | 11 | ENDPOINTS = LATEST + v1 12 | 13 | __all__ = ['ENDPOINTS'] 14 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/endpoints/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from app.endpoints.v1 import example 2 | 3 | from fastapi_serviceutils.app.endpoints import set_version_endpoints 4 | 5 | ENDPOINTS = set_version_endpoints( 6 | endpoints=[example], 7 | version='v1', 8 | prefix_template='/api/{version}{route}' 9 | ) 10 | 11 | __all__ = ['ENDPOINTS'] 12 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/endpoints/v1/errors.py: -------------------------------------------------------------------------------- 1 | """Contain error-messages to be used for the endpoints of v1.""" 2 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/endpoints/v1/example.py: -------------------------------------------------------------------------------- 1 | from app.endpoints.v1.models import Example as Output 2 | from app.endpoints.v1.models import GetExample as Input 3 | from fastapi import APIRouter 4 | from fastapi import Body 5 | from starlette.requests import Request 6 | 7 | from fastapi_serviceutils.app import create_id_logger 8 | from fastapi_serviceutils.app import Endpoint 9 | 10 | ENDPOINT = Endpoint(router=APIRouter(), route='/example', version='v1') 11 | SUMMARY = 'Example request.' 12 | EXAMPLE = Body(..., example={'msg': 'some message.'}) 13 | 14 | 15 | @ENDPOINT.router.post('/', response_model=Output, summary=SUMMARY) 16 | async def example(request: Request, params: Input = EXAMPLE) -> Output: 17 | _, log = create_id_logger(request=request, endpoint=ENDPOINT) 18 | log.debug(f'received request for {request.url} with params {params}.') 19 | return Output(msg=params.msg) 20 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/endpoints/v1/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class GetExample(BaseModel): 5 | msg: str 6 | 7 | 8 | class Example(BaseModel): 9 | msg: str 10 | 11 | 12 | __all__ = ['Example', 'GetExample'] 13 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/app/main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import NoReturn 3 | 4 | from app import __version__ 5 | from app.endpoints import ENDPOINTS 6 | 7 | from fastapi_serviceutils import make_app 8 | 9 | app = make_app( 10 | config_path=Path(__file__).with_name('config.yml'), 11 | version=__version__, 12 | endpoints=ENDPOINTS, 13 | enable_middlewares=['trusted_hosts', 14 | 'log_exception'], 15 | additional_middlewares=[] 16 | ) 17 | 18 | 19 | def main() -> NoReturn: 20 | import uvicorn 21 | uvicorn.run(app, host='0.0.0.0', port=app.config.service.port) 22 | 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "exampleservice" 3 | version = "0.1.0" 4 | description = "Exampleservice to demonstrate usage of fastapi-serviceutils." 5 | authors = ["Dummy User "] 6 | readme = "README.md" 7 | include = ["README.md", "app/config.yml"] 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.7,<4" 11 | fastapi-serviceutils = ">=2" 12 | 13 | [tool.poetry.dev-dependencies] 14 | autoflake = ">=1.3" 15 | coverage-badge = ">=1" 16 | flake8 = ">=3.7" 17 | ipython = ">=7.8" 18 | isort = ">=4.3" 19 | jedi = ">=0.14" 20 | neovim = ">=0.3.1" 21 | pre-commit = ">=1.18.3" 22 | pudb = ">=2019.1" 23 | pygments = ">=2.4" 24 | pytest = ">=5" 25 | pytest-asyncio = ">=0.10" 26 | pytest-cov = ">=2" 27 | pytest-xdist = ">=1.30" 28 | sphinx = ">=2" 29 | sphinx-autodoc-typehints = ">=1.6" 30 | sphinx-rtd-theme = ">=0.4.3" 31 | yapf = ">=0.27" 32 | 33 | [tool.poetry.extras] 34 | devs = [ 35 | "autoflake", "coverage", "coverage-badge", "flake8", "ipython", "isort", 36 | "jedi", "neovim", "pre-commit", "pudb", "pygments", "pytest", 37 | "pytest-asyncio", "pytest-cov", "pytest-xdist", "sphinx", 38 | "sphinx-autodoc-typehints", "sphinx-rtd-theme", "yapf" 39 | ] 40 | 41 | [tool.dephell.devs] 42 | from = {format = "poetry", path = "pyproject.toml"} 43 | envs = ["main", "devs"] 44 | 45 | [tool.dephell.main] 46 | from = {format = "poetry", path = "pyproject.toml"} 47 | to = {format = "setuppy", path = "setup.py"} 48 | envs = ["main"] 49 | versioning = "semver" 50 | 51 | [tool.dephell.lock] 52 | from = {format = "poetry", path = "pyproject.toml"} 53 | to = {format = "poetrylock", path = "poetry.lock"} 54 | 55 | [tool.poetry.scripts] 56 | exampleservice = "app.main:main" 57 | 58 | [build-system] 59 | requires = ["poetry>=0.12"] 60 | build-backend = "poetry.masonry.api" 61 | -------------------------------------------------------------------------------- /docs/_static/exampleservice/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skallfass/fastapi_serviceutils/8033450fc556396c735fb7c783ebe36e28a60ac0/docs/_static/exampleservice/tests/__init__.py -------------------------------------------------------------------------------- /docs/_static/exampleservice/tests/service_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from app.main import app 3 | 4 | from fastapi_serviceutils.app.service_config import Config 5 | from fastapi_serviceutils.utils.tests.endpoints import json_endpoint 6 | 7 | 8 | def test_endpoint_example(): 9 | json_endpoint( 10 | application=app, 11 | endpoint='/api/v1/example/', 12 | payload={'msg': 'test'}, 13 | expected={'msg': 'test'} 14 | ) 15 | 16 | 17 | @pytest.mark.parametrize( 18 | 'endpoint, status_code', 19 | [ 20 | ('/api/v1/example', 21 | 307), 22 | ('/api/', 23 | 404), 24 | ('/api/v1/', 25 | 404), 26 | ('/api/v1/example/', 27 | 200), 28 | ] 29 | ) 30 | def test_endpoint_invalid(endpoint, status_code): 31 | json_endpoint( 32 | application=app, 33 | endpoint=endpoint, 34 | status_code=status_code, 35 | payload={'msg': 'test'} 36 | ) 37 | -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skallfass/fastapi_serviceutils/8033450fc556396c735fb7c783ebe36e28a60ac0/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.app.endpoints.default.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.app.endpoints.default package 2 | =================================================== 3 | 4 | .. automodule:: fastapi_serviceutils.app.endpoints.default 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | fastapi\_serviceutils.app.endpoints.default.alive module 13 | -------------------------------------------------------- 14 | 15 | .. automodule:: fastapi_serviceutils.app.endpoints.default.alive 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | fastapi\_serviceutils.app.endpoints.default.config module 21 | --------------------------------------------------------- 22 | 23 | .. automodule:: fastapi_serviceutils.app.endpoints.default.config 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | fastapi\_serviceutils.app.endpoints.default.models module 29 | --------------------------------------------------------- 30 | 31 | .. automodule:: fastapi_serviceutils.app.endpoints.default.models 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.app.endpoints.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.app.endpoints package 2 | =========================================== 3 | 4 | .. automodule:: fastapi_serviceutils.app.endpoints 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | 14 | fastapi_serviceutils.app.endpoints.default 15 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.app.handlers.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.app.handlers package 2 | ========================================== 3 | 4 | .. automodule:: fastapi_serviceutils.app.handlers 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.app.middlewares.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.app.middlewares package 2 | ============================================= 3 | 4 | .. automodule:: fastapi_serviceutils.app.middlewares 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.app.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.app package 2 | ================================= 3 | 4 | .. automodule:: fastapi_serviceutils.app 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | 14 | fastapi_serviceutils.app.endpoints 15 | fastapi_serviceutils.app.handlers 16 | fastapi_serviceutils.app.middlewares 17 | 18 | Submodules 19 | ---------- 20 | 21 | fastapi\_serviceutils.app.logger module 22 | --------------------------------------- 23 | 24 | .. automodule:: fastapi_serviceutils.app.logger 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | fastapi\_serviceutils.app.service\_config module 30 | ------------------------------------------------ 31 | 32 | .. automodule:: fastapi_serviceutils.app.service_config 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.cli.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.cli package 2 | ================================= 3 | 4 | .. automodule:: fastapi_serviceutils.cli 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | fastapi\_serviceutils.cli.create\_service module 13 | ------------------------------------------------ 14 | 15 | .. automodule:: fastapi_serviceutils.cli.create_service 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils package 2 | ============================= 3 | 4 | .. automodule:: fastapi_serviceutils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | 14 | fastapi_serviceutils.app 15 | fastapi_serviceutils.cli 16 | fastapi_serviceutils.utils 17 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.utils.docs.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.utils.docs package 2 | ======================================== 3 | 4 | .. automodule:: fastapi_serviceutils.utils.docs 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | fastapi\_serviceutils.utils.docs.apidoc module 13 | ---------------------------------------------- 14 | 15 | .. automodule:: fastapi_serviceutils.utils.docs.apidoc 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.utils.external_resources.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.utils.external\_resources package 2 | ======================================================= 3 | 4 | .. automodule:: fastapi_serviceutils.utils.external_resources 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | fastapi\_serviceutils.utils.external\_resources.dbs module 13 | ---------------------------------------------------------- 14 | 15 | .. automodule:: fastapi_serviceutils.utils.external_resources.dbs 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | fastapi\_serviceutils.utils.external\_resources.services module 21 | --------------------------------------------------------------- 22 | 23 | .. automodule:: fastapi_serviceutils.utils.external_resources.services 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.utils.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.utils package 2 | =================================== 3 | 4 | .. automodule:: fastapi_serviceutils.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | 14 | fastapi_serviceutils.utils.docs 15 | fastapi_serviceutils.utils.external_resources 16 | fastapi_serviceutils.utils.tests 17 | -------------------------------------------------------------------------------- /docs/api/fastapi_serviceutils.utils.tests.rst: -------------------------------------------------------------------------------- 1 | fastapi\_serviceutils.utils.tests package 2 | ========================================= 3 | 4 | .. automodule:: fastapi_serviceutils.utils.tests 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | fastapi\_serviceutils.utils.tests.endpoints module 13 | -------------------------------------------------- 14 | 15 | .. automodule:: fastapi_serviceutils.utils.tests.endpoints 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | -------------------------------------------------------------------------------- /docs/api/modules.rst: -------------------------------------------------------------------------------- 1 | fastapi_serviceutils 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | fastapi_serviceutils 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Contain the configuration for the sphinx-documentation of the service.""" 3 | 4 | project = 'fastapi-serviceutils' 5 | copyright = '2019, Simon Kallfass (MIT-License)' 6 | author = 'Simon Kallfass' 7 | release = version = '2.0.0' 8 | 9 | extensions = [ 10 | 'sphinx.ext.autodoc', 11 | 'sphinx.ext.intersphinx', 12 | 'sphinx.ext.napoleon', 13 | 'sphinx_autodoc_typehints', 14 | 'sphinx.ext.viewcode', 15 | 'sphinx_rtd_theme' 16 | ] 17 | 18 | templates_path = ['_templates'] 19 | 20 | exclude_patterns = [ 21 | '_build', 22 | 'Thumbs.db', 23 | '.DS_Store', 24 | 'api/modules.rst', 25 | ] 26 | 27 | html_theme = 'sphinx_rtd_theme' 28 | pygments_style = 'solarized-dark' 29 | 30 | html_static_path = ['_static'] 31 | 32 | html_logo = '_static/logo.png' 33 | html_theme_options = { 34 | 'canonical_url': '', 35 | 'analytics_id': '', 36 | 'logo_only': False, 37 | 'display_version': True, 38 | 'prev_next_buttons_location': 'bottom', 39 | 'style_external_links': False, 40 | 'collapse_navigation': False, 41 | 'sticky_navigation': True, 42 | 'navigation_depth': 5, 43 | 'includehidden': True, 44 | 'titles_only': False 45 | } 46 | 47 | intersphinx_mapping = { 48 | 'python': ('https://docs.python.org/3', 49 | None), 50 | } 51 | 52 | napoleon_google_docstring = True 53 | napoleon_numpy_docstring = False 54 | napoleon_include_init_with_doc = True 55 | napoleon_use_param = True 56 | -------------------------------------------------------------------------------- /docs/create_service.rst: -------------------------------------------------------------------------------- 1 | create_service 2 | ^^^^^^^^^^^^^^ 3 | 4 | .. include:: sources.rst 5 | 6 | Create new service following the structure as described in the 7 | fastapi_serviceutils documentation. 8 | Using Cookiecutter_ to create the new folder. 9 | 10 | 11 | .. literalinclude:: create_service_help.txt 12 | -------------------------------------------------------------------------------- /docs/create_service_help.txt: -------------------------------------------------------------------------------- 1 | usage: create_service [-h] -n SERVICE_NAME -p SERVICE_PORT -a AUTHOR -e 2 | AUTHOR_EMAIL -ep ENDPOINT -o OUTPUT_DIR 3 | 4 | create new service based on fastapi using fastapi_serviceutils. 5 | 6 | optional arguments: 7 | -h, --help show this help message and exit 8 | -n SERVICE_NAME, --service_name SERVICE_NAME 9 | the name of the service to create. ATTENTION: only 10 | ascii-letters, "_" and digits are allowed. Must not 11 | start with a digit! 12 | -p SERVICE_PORT, --service_port SERVICE_PORT 13 | the port for the service to listen. 14 | -a AUTHOR, --author AUTHOR 15 | the name of the author of the service. 16 | -e AUTHOR_EMAIL, --author_email AUTHOR_EMAIL 17 | the email of the author of the service. 18 | -ep ENDPOINT, --endpoint ENDPOINT 19 | the name of the endpoint for the service to create. 20 | ATTENTION: only lower ascii-letters, "_" and digits 21 | are allowed. Must not start with a digit! 22 | -o OUTPUT_DIR, --output_dir OUTPUT_DIR 23 | -------------------------------------------------------------------------------- /docs/dependency_management.rst: -------------------------------------------------------------------------------- 1 | .. include:: sources.rst 2 | 3 | 4 | Dependency management 5 | --------------------- 6 | 7 | We use Poetry_ including the dependency definition inside the 8 | ``pyproject.toml`` and ``python-venv`` for environment management. 9 | Additionally we use Dephell_ and ``make`` for easier workflow. 10 | 11 | .. code-block:: bash 12 | :caption: dependency-management files 13 | 14 | 15 | ├── ... 16 | ├── poetry.lock 17 | ├── pyproject.toml 18 | ├── .python-version 19 | └── ... 20 | 21 | * ``pyproject.toml``: stores what dependencies are required in which versions. 22 | Required by Dephell_ and Poetry_. 23 | * ``poetry.lock``: locked definition of installed packages and their versions 24 | of currently used devs-environment. Created by Poetry_ using ``make 25 | init``, ``make update``, ``make tests`` or ``make finalize``. 26 | * ``.python-version``: the version of the python-interpreter used for this 27 | project. Created by ``python-venv`` using ``make init``, required by 28 | Poetry_ and Dephell_. 29 | -------------------------------------------------------------------------------- /docs/deployment.rst: -------------------------------------------------------------------------------- 1 | Deployment 2 | ---------- 3 | 4 | .. include:: sources.rst 5 | 6 | For more detailed information about deployment of fastapi-based services see 7 | `FastAPI deployment`_ 8 | 9 | 10 | Services based on ``fastapi-serviceutils`` can be easily deployed inside 11 | a docker-container. 12 | 13 | 14 | Before deployment you need to: 15 | 16 | * **update the dependencies** 17 | * **run all tests** 18 | * **create the current** ``requirements.txt`` 19 | * **ensure the** ``docker-compose.yml`` **is defined correctly including the** 20 | ``environment-variables`` 21 | 22 | To run these tasks run: 23 | 24 | .. code-block:: bash 25 | 26 | make finalize 27 | 28 | To run the service using docker-compose customize the ``docker-compose.yml`` 29 | and run: 30 | 31 | .. code-block:: bash 32 | 33 | sudo docker-compose up -d 34 | 35 | 36 | .. include:: deployment_basics.rst 37 | -------------------------------------------------------------------------------- /docs/deployment_basics.rst: -------------------------------------------------------------------------------- 1 | Basics 2 | ^^^^^^ 3 | 4 | .. include:: sources.rst 5 | 6 | Docker 7 | """""" 8 | 9 | The basic Dockerfile_ should look like: 10 | 11 | .. literalinclude:: _static/example_dockerfile 12 | 13 | 14 | Docker-compose 15 | """""""""""""" 16 | 17 | The service can be deployed with `Docker compose`_ using the 18 | `Docker compose file`_: 19 | 20 | 21 | .. literalinclude:: _static/example_docker_compose.yml 22 | :caption: an example for a ``docker-compose.yml`` for a service using 23 | ``fastapi_serviceutils``. 24 | 25 | 26 | Environment-variables 27 | """"""""""""""""""""" 28 | 29 | Setting environment-variables overwrites the default values defined in the 30 | :ref:`config`. 31 | 32 | Please ensure to use the **environment-variables** (`Environment variable`_) 33 | if you want to overwrite some default-settings of the service. 34 | 35 | The environment-variables to use should be defined inside the ``config.yml``. 36 | Set the values of the environment-variables inside the ``docker-compose.yml``. 37 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | .. include:: sources.rst 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | getting_started.rst 10 | dependency_management.rst 11 | testing.rst 12 | documentation.rst 13 | -------------------------------------------------------------------------------- /docs/doc_requirements.txt: -------------------------------------------------------------------------------- 1 | autoflake>=1.3 2 | cookiecutter>=1.6 3 | coverage-badge>=1 4 | databases[postgresql]>=0.2 5 | fastapi[all]>=0.44 6 | flake8>=3.7 7 | ipython>=7.8 8 | jedi>=0.14 9 | loguru>=0.4 10 | neovim>=0.3.1 11 | psycopg2>=2.8 12 | pudb>=2019.1 13 | pygments>=2.4 14 | pylint>=2.4.3 15 | pytest>=5 16 | pytest-asyncio>=0.10 17 | pytest-cov>=2 18 | pytest-xdist>=1.30 19 | requests>=2.22.0 20 | sphinx>=2 21 | sphinx-autodoc-typehints>=1.6 22 | sphinx-rtd-theme>=0.4.3 23 | sqlalchemy>=1.3 24 | toolz>=0.10 25 | yapf>=0.27 26 | -------------------------------------------------------------------------------- /docs/documentation.rst: -------------------------------------------------------------------------------- 1 | .. include:: sources.rst 2 | 3 | 4 | Documentation 5 | ------------- 6 | The project's developer documentation is written using Sphinx_. 7 | 8 | The documentation sources can be found in the ``docs`` subdirectory. 9 | They are using restructuredText_-files. 10 | 11 | The API-documentation is auto-generated from the docstrings of modules, classes, 12 | and functions. 13 | For documentation inside the source-code the `Google docstring standard`_ is 14 | used. 15 | 16 | To generate the documentation, run 17 | 18 | .. code-block:: bash 19 | 20 | make docs 21 | 22 | The created documentation (as html files) will be inside the ``docs/_build`` 23 | directory. 24 | 25 | There is also a swagger-documentation to be used for users of the service. 26 | After starting the service the documentation can be viewed at: 27 | 28 | * http://0.0.0.0:/docs 29 | * http://0.0.0.0:/redoc 30 | 31 | The sphinx-documentation can be viewed after service-started and docs created 32 | at http://0.0.0.0:/apidoc/index.html. 33 | 34 | .. code-block:: bash 35 | :caption: documentation related files 36 | 37 | 38 | ├── ... 39 | ├── docs 40 | │   ├── _build 41 | │   │  └── ... 42 | │   ├── conf.py 43 | │   ├── development.rst 44 | │   ├── index.rst 45 | │   ├── .rst 46 | │   └── _static 47 | │   ├── coverage.svg 48 | │   └── logo.png 49 | ├── ... 50 | ├── README.md 51 | └── ... 52 | 53 | -------------------------------------------------------------------------------- /docs/exampleservice.rst: -------------------------------------------------------------------------------- 1 | .. _exampleservice: 2 | 3 | exampleservice 4 | ============== 5 | 6 | .. include:: sources.rst 7 | 8 | The easiest way to explain how to use fastapi-serviceutils is to demonstrate 9 | usage inside an exampleservice. 10 | Here we will explain the parts of the service and which functions and classes 11 | when to use. 12 | 13 | 14 | Creating new service 15 | -------------------- 16 | 17 | To create a new service we use the tool ``create_service`` which is available 18 | after installing fastapi-serviceutils. 19 | 20 | .. code:: bash 21 | 22 | create_service -n exampleservice \ 23 | -p 50001 \ 24 | -a "Dummy User" \ 25 | -e dummy.user@something.info \ 26 | -ep example \ 27 | -o /tmp 28 | 29 | This creates the service **exampleservice** inside the folder 30 | **/tmp/exampleservice**. 31 | As author with email we define **Dummy User** and 32 | **dummy.user@something.info**. 33 | The initial endpoint we want to create is **example**. 34 | The service should listen to port **50001**. 35 | 36 | If we change into the created directory we will have the following 37 | folder-structure: 38 | 39 | .. code:: bash 40 | 41 | exampleservice 42 | ├── app 43 | │   ├── config.yml 44 | │   ├── endpoints 45 | │   │   ├── __init__.py 46 | │   │   └── v1 47 | │   │   ├── errors.py 48 | │   │   ├── example.py 49 | │   │   ├── __init__.py 50 | │   │   └── models.py 51 | │   ├── __init__.py 52 | │   └── main.py 53 | ├── .codespell-ignore-words.txt 54 | ├── docker-compose.yml 55 | ├── Dockerfile 56 | ├── docs 57 | │   └── ... 58 | ├── .gitignore 59 | ├── Makefile 60 | ├── .pre-commit-config.yaml 61 | ├── pyproject.toml 62 | ├── .python-version 63 | ├── README.md 64 | ├── setup.cfg 65 | ├── tests 66 | │   └── __init__.py 67 | └── .tmuxp.yml 68 | 69 | 70 | The files ``docker-compose.yml`` and ``Dockerfile`` are required for 71 | deployment of the service as docker-container. 72 | 73 | ``.tmuxp.yml`` is used for development of the service if you prefer to develop 74 | inside tmux in combination with for example vim or emacs. 75 | 76 | The ``.python-version`` defines which python-version this service uses and is 77 | used by poetry / dephell workflow inside virtual-environments. 78 | 79 | The ``pyproject.toml`` is used for dependency-management and package-creation. 80 | 81 | ``setup.cfg`` contains configurations for tools used during development like 82 | yapf, flake8, pytest, etc. 83 | 84 | The ``.pre-commit-config.yaml`` allows the usage of pre-commit and is also 85 | used in the make command ``make check``. 86 | It enables running of multiple linters, checkers, etc. to ensure a fixed 87 | codestyle. 88 | 89 | The ``Makefile`` contains helper command like initializing the project, 90 | updating the virtual-environment, running tests, etc. 91 | 92 | Because codespell is used inside the configuration of pre-commit, the file 93 | ``.codespell-ignore-words.txt`` is used to be able to define words to be 94 | ignored during check with codespell. 95 | 96 | 97 | Initialising project 98 | ^^^^^^^^^^^^^^^^^^^^ 99 | 100 | To initialise the project after creation we run: 101 | 102 | .. code:: bash 103 | 104 | make init 105 | 106 | This creates the virtual-environment and installs the dependencies as defined 107 | in the ``pyproject.toml``. 108 | It also initialises the project as a git-folder and creates the initial 109 | commit. 110 | 111 | We now activate the poetry-shell to enable the environment: 112 | 113 | .. code:: bash 114 | 115 | poetry shell 116 | 117 | 118 | .. Attention:: 119 | 120 | Please ensure to always enable the poetry-shell before development using: 121 | 122 | .. code:: bash 123 | 124 | poetry shell 125 | 126 | The Makefile assumes the environment is activated on usage. 127 | 128 | 129 | Folder-structure 130 | ---------------- 131 | 132 | Following shows code-relevant files for an exampleservice as created using the 133 | create_service-tool of fastapi-serviceutils. 134 | 135 | .. code:: bash 136 | 137 | exampleservice 138 | ├── app 139 | │   ├── config.yml 140 | │   ├── endpoints 141 | │   │   ├── __init__.py 142 | │   │   └── v1 143 | │   │   ├── errors.py 144 | │   │   ├── example.py 145 | │   │   ├── __init__.py 146 | │   │   └── models.py 147 | │   ├── __init__.py 148 | │   └── main.py 149 | ├── pyproject.toml 150 | └── tests 151 | ├── __init__.py 152 | └── service_test.py 153 | 154 | 155 | pyproject.toml 156 | -------------- 157 | 158 | The dependencies and definitions like the package-name, version, etc. are 159 | defined inside the ``pyproject.toml``. 160 | This file is used by Poetry_ and Dephell_. 161 | Following the ``pyproject.toml`` for our exampleservice: 162 | 163 | .. literalinclude:: _static/exampleservice/pyproject.toml 164 | :caption: the ``pyproject.toml`` of the exampleservice. 165 | 166 | 167 | app/config.yml 168 | -------------- 169 | 170 | The service is configured using a config-file (``config.yml``). 171 | It is possible to overwrite these setting using environment-variables. 172 | An example for the ``config.yml`` of the exampleservice is shown below: 173 | 174 | .. literalinclude:: _static/exampleservice/app/config.yml 175 | :caption: ``config.yml`` of exampleservice. 176 | 177 | The config contains four main sections: 178 | 179 | * **service** 180 | * **external_resources** 181 | * **logger** 182 | * **available_environment_variables** 183 | 184 | 185 | config: [service] 186 | ^^^^^^^^^^^^^^^^^ 187 | 188 | Inside this section we define the name of the service ``name``. 189 | This name is used for the **swagger-documentation** and **extraction of the 190 | environment-variables**. 191 | 192 | The ``mode`` define the **runtime-mode** of the service. 193 | This mode can be overwritten with the environment-variable 194 | ``EXAMPLESERVICE__SERVICE__MODE`` (where ``'EXAMPLESERVICE'`` is the name of 195 | the service, meaning if you have a service named ``SOMETHING`` the 196 | environment-variable would be ``SOMETHING__SERVICE__MODE``). 197 | 198 | The ``port`` configure the **port the service will listen to**. 199 | This can also be overwritten using the environment variable 200 | ``EXAMPLESERVICE__SERVICE__PORT``. 201 | 202 | The ``description`` is used for the swagger-documentation. 203 | 204 | To define the folder where the to find the **apidoc to serve by route** 205 | ``/api/apidoc/index.html`` the keyword ``apidoc_dir`` is used. 206 | 207 | ``readme`` defines where to get the readme from to be used as main description 208 | for the swagger-documentation at ``/docs`` / ``/redoc``. 209 | 210 | To controll if only specific hosts are allowed to controll the service we use 211 | ``allowed_hosts``. 212 | Per default a service would allow all hosts (``'*'``) but this can be 213 | customized here in the config. 214 | 215 | To define which default endpoints should be included in our service we use 216 | ``use_default_endpoints``. 217 | Currently we support the default endpoints ``/api/alive`` (inside config: 218 | ``'alive'``) and ``/api/config`` (inside config: ``'alive'``). 219 | 220 | 221 | config: [external_resources] 222 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 223 | 224 | Inside this section external dependencies (resources) are defines. 225 | A service can depend on other services, databases, remote-connections or 226 | files / folders. 227 | 228 | Dependencies to other services should be defined inside ``services``. 229 | Database connections inside ``databases`` (currently only postgres is 230 | supported). 231 | If any other dependency exist define it in ``other``. 232 | 233 | Defined services can be accessed in the code using 234 | ``app.config.external_resources.services`` or 235 | ``ENDPOINT.config.external_resources.services`` depending if you are in a main 236 | part of the app or inside an endpoint. 237 | 238 | Databases are automatically included into the ``startup`` and ``shutdown`` 239 | handlers. 240 | You can access the database connection using ``app.databases['DATABASE_NAME']`` 241 | or ``ENDPOINT.databases['DATABASE_NAME']`` depending if you are in a main part 242 | of the app or inside an endpoint. 243 | 244 | 245 | config: [logger] 246 | ^^^^^^^^^^^^^^^^ 247 | 248 | All settings inside this section are default Loguru_ settings to configure the 249 | logger. 250 | You can control where to log (``path``) and how the logfile should be named 251 | (``filename``). 252 | Also which minimum level to log (``level``). 253 | To control when to rotate the logfile use ``rotation``. 254 | ``retention`` defines when to delete old logfiles. 255 | The ``format`` defines the format to be used for log-messages. 256 | 257 | 258 | config: [available_environment_variables] 259 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 260 | 261 | The environment-variables are seperated into three types: 262 | 263 | * ``env_vars`` 264 | * ``external_resources_env_vars`` 265 | * ``rules_env_vars`` 266 | 267 | Here you can control which environment-variables to use if they are set. 268 | 269 | The environment-variables are named like the following: 270 | ``____``. 271 | The servicename would be ``'EXAMPLESERVICE'`` in our example. 272 | The major-section is one of: 273 | 274 | * ``'SERVICE'`` 275 | * ``'LOGGER'`` 276 | * ``'EXTERNAL_RESOURCES'`` 277 | 278 | ``env_vars`` control the sections ``service`` and ``logger``. 279 | ``external_resources_env_vars`` control the configurations inside the section 280 | ``external_resources``. 281 | The ``rules_env_vars`` should overwrite settings of a ruleset of the service. 282 | Such a ruleset defines constants and other rules for the logic of the 283 | endpoints. 284 | For example a default time-range for your pandas dataframes, etc. 285 | Currently this is not implemented, so you would have to use these definitions 286 | yourself to overwrite your ruleset-definitions. 287 | 288 | 289 | app/\__init\__.py 290 | ----------------- 291 | 292 | Inside the ``__init__.py`` file of the app we only define the version of our 293 | service. 294 | 295 | .. Note:: 296 | 297 | We use semantic-versioning style for services based on 298 | fastapi-serviceutils. 299 | 300 | This means we have the following version-number: 301 | ``..``. 302 | 303 | For details about semantic-versioning see Semver_. 304 | 305 | If we bump the version using either ``dephell bump {major, minor, fix}`` or 306 | ``poetry version {major, minor, patch}``, both the version defined here, and 307 | the version defined inside the ``pyproject.toml`` will be increased. 308 | 309 | .. literalinclude:: _static/exampleservice/app/__init__.py 310 | :caption: ``__init__.py`` of app. 311 | 312 | 313 | app/main.py 314 | ----------- 315 | 316 | Inside this file we glue all parts of our service together. 317 | 318 | Here the ``app`` is created which is used either in development inside the 319 | function ``main`` or in production using ``uvicorn`` from command line (or 320 | docker-container). 321 | 322 | .. literalinclude:: _static/exampleservice/app/main.py 323 | :caption: ``main.py`` of app. 324 | 325 | We define where to collect the config-file of the service from, the version of 326 | the service and which endpoints and middlewares to use. 327 | 328 | 329 | app/endpoints/v1/example.py 330 | --------------------------- 331 | 332 | The following shows the example-endpoint we created: 333 | 334 | .. literalinclude:: _static/exampleservice/app/endpoints/v1/example.py 335 | :caption: ``example.py`` in version 1. Define the endpoint example. 336 | 337 | The ``ENDPOINT`` includes the ``router``, ``route`` and the ``version`` of our 338 | endpoint. 339 | 340 | Inside the endpoint-function we create a new bound logger with the request-id 341 | of the request to allow useful traceback. 342 | 343 | .. Note:: 344 | 345 | Defining endpoints like this allows our worklow with endpoint-versioning 346 | and usage of :func:`fastapi_serviceutils.endpoints.set_version_endpoints` 347 | inside ``app/endpoints/v1/__init__.py`` and ``app/endpoints/__init__.py``. 348 | 349 | 350 | app/endpoints/v1/models.py 351 | -------------------------- 352 | 353 | The models.py contains models for the endpoints in version 1 of our 354 | exampleservice. 355 | 356 | For each endpoint we create the model for the input (request) and the model 357 | for the output (response). 358 | 359 | The models are of type :class:`pydantic.BaseModel` 360 | 361 | .. literalinclude:: _static/exampleservice/app/endpoints/v1/models.py 362 | :caption: ``models.py`` of endpoints of version 1. 363 | 364 | More complex models could look like the following: 365 | 366 | .. code:: python 367 | 368 | """ 369 | In special cases also an ``alias_generator`` has to be defined. 370 | An example for such a special case is the attribute ``schema`` of 371 | :class:`SpecialParams`. The schema is already an attribute of a BaseModel, 372 | so it can't be used and an alias is required. 373 | 374 | To be able to add post-parse-methods the pydantic ``dataclass`` can be 375 | used. 376 | An example for this can be seen in :class:`Complex`. 377 | """ 378 | 379 | from pydantic import BaseModel 380 | from pydantic import Schema 381 | from pydantic.dataclasses import dataclass 382 | 383 | @dataclass 384 | class Complex: 385 | """Represent example model with attribute-change of model after init.""" 386 | accuracy: str 387 | 388 | def __post_init_post_parse__(self) -> NoReturn: 389 | """Overwrite self.accuracy with a mapping as defined below.""" 390 | accuracy_mapping = { 391 | 'something': 's', 392 | 'match': 'm', 393 | } 394 | self.accuracy = accuracy_mapping[self.accuracy] 395 | 396 | def _alias_for_special_model_attribute(alias: str) -> str: 397 | """Use as ``alias_generator`` for models with special attribute-names.""" 398 | return alias if not alias.endswith('_') else alias[:-1] 399 | 400 | class SpecialParams(BaseModel): 401 | """Represent example model with special attribute name requiring alias.""" 402 | msg: str 403 | schema_: str = Schema(None, alias='schema') 404 | 405 | class Config: 406 | """Required for special attribute ``schema``.""" 407 | alias_generator = _alias_for_special_model_attribute 408 | 409 | 410 | app/endpoints/v1/\__init\__.py 411 | ------------------------------ 412 | 413 | Inside this file we include our example-endpoint to the version 1 endpoints. 414 | 415 | .. Note:: 416 | 417 | If additional endpoints are available, these should be added here, too. 418 | 419 | The created ``ENDPOINTS`` is used inside ``app/endpoints/__init__.py`` later. 420 | 421 | .. Note:: 422 | 423 | If we would increase our version to version 2 and we want to change the 424 | endpoint ``example`` we would add an additional folder inside 425 | ``app/endpoints`` named ``v2`` and place the new version files there. 426 | 427 | .. literalinclude:: _static/exampleservice/app/endpoints/v1/__init__.py 428 | :caption: ``__init__.py`` of v1. 429 | 430 | 431 | app/endpoints/\__init\__.py 432 | --------------------------- 433 | 434 | In this file we import all endpoint-versions like in this example 435 | ``from app.endpoints.v1 import ENDPOINTS as v1``. 436 | 437 | .. Note:: 438 | 439 | If we would have an additional version 2 we would also add ``from 440 | app.endpoints.v2 import ENDPOINTS as v2``. 441 | 442 | Then we use :func:`fastapi_serviceutils.endpoints.set_version_endpoints` with 443 | the latest version endpoints to create ``LATEST``. 444 | 445 | .. Note:: 446 | 447 | If we would have version 2, too we would replace parameter ``endpoints`` 448 | with ``v2``. 449 | 450 | The ``ENDPOINTS`` is a list of all available versions. 451 | 452 | These ``ENDPOINTS`` are used inside ``app/main.py`` to include them to the 453 | service. 454 | 455 | .. literalinclude:: _static/exampleservice/app/endpoints/__init__.py 456 | :caption: ``__init__.py`` of endpoints. 457 | 458 | 459 | tests 460 | ----- 461 | 462 | The tests for the exampleservice are using Pytest_. 463 | We also used the testutils of ``fastapi-serviceutils``. 464 | An example for simple endpoint tests of our exampleservice: 465 | 466 | .. literalinclude:: _static/exampleservice/tests/service_test.py 467 | :caption: ``tests/service_test.py`` 468 | -------------------------------------------------------------------------------- /docs/external_resources.rst: -------------------------------------------------------------------------------- 1 | External resources 2 | ================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | external_resources_databases.rst 8 | external_resources_services.rst 9 | -------------------------------------------------------------------------------- /docs/external_resources_databases.rst: -------------------------------------------------------------------------------- 1 | Databases 2 | ========= 3 | 4 | 5 | config.yml 6 | ---------- 7 | 8 | If we use a database in our service we declare the connection info in the 9 | ``config.yml`` of the service like the following: 10 | 11 | .. code-block:: yaml 12 | :caption: ``app/config.yml`` 13 | 14 | ... 15 | external_resources: 16 | services: null 17 | databases: 18 | userdb: 19 | dsn: 'postgresql://postgres:1234@localhost:5434/userdb' 20 | databasetype: 'postgres' 21 | min_size: 5 22 | max_size: 20 23 | other: null 24 | ... 25 | 26 | For each database we want to use in our service, we define a new item inside 27 | ``databases``. 28 | The **key** will be the name of our database. 29 | The connection itself is defined as ``dsn``. 30 | The databasetype defines the type of the database we are using. 31 | This setting is for future releases of fastapi-serviceutils. 32 | Currently we only support ``postgres`` and this setting has no effect. 33 | ``min_size`` and ``max_size`` define the minimum and maximum amount of 34 | connections to open to the database. 35 | 36 | 37 | app/endpoints/v1/dbs.py 38 | ----------------------- 39 | 40 | Inside the module ``dbs.py`` we define our datatables like the following: 41 | 42 | .. code-block:: python 43 | :caption: ``app/endpoints/v1/dbs.py`` 44 | 45 | from sqlalchemy import Boolean 46 | from sqlalchemy import Column 47 | from sqlalchemy import insert 48 | from sqlalchemy import Integer 49 | from sqlalchemy import String 50 | from sqlalchemy.ext.declarative import declarative_base 51 | 52 | Base = declarative_base() 53 | 54 | class User(Base): 55 | __tablename__ = 'users' 56 | 57 | id = Column(Integer, primary_key=True, index=True) 58 | email = Column(String, unique=True, index=True) 59 | password = Column(String) 60 | 61 | 62 | app/endpoints/v1/models.py 63 | -------------------------- 64 | 65 | As for each endpoint we declare the input- and output-models we are using in 66 | our new endpoints like the following: 67 | 68 | .. code-block:: python 69 | :caption: ``app/endpoints/v1/models.py`` 70 | 71 | from pydantic import BaseModel 72 | 73 | class InsertUser(BaseModel): 74 | email: str 75 | password: str 76 | 77 | class Inserted(BaseModel): 78 | msg: bool = True 79 | 80 | class User(BaseModel): 81 | id: int 82 | email: str 83 | password: str 84 | 85 | 86 | app/endpoints/v1/insert_user.py 87 | ------------------------------- 88 | 89 | .. code-block:: python 90 | :caption: ``app/endpoints/v1/insert_user.py`` 91 | 92 | from fastapi import Body 93 | from fastapi import APIRouter 94 | from fastapi_serviceutils.app import Endpoint 95 | from fastapi_serviceutils.app import create_id_logger 96 | from sqlalchemy import insert 97 | 98 | from app.endpoints.v1.dbs import User 99 | from app.endpoints.v1.models import InsertUser as Input 100 | from app.endpoints.v1.models import Inserted as Output 101 | 102 | ENDPOINT = Endpoint(router=APIRouter(), route='/insert_user', version='v1') 103 | SUMMARY = 'Example request.' 104 | EXAMPLE = Body( 105 | ..., 106 | example={ 107 | 'email': 'dummy.user@something.info' 108 | 'password': 'an3xampleP4ssword' 109 | } 110 | ) 111 | 112 | @ENDPOINT.router.post('/', response_model=Output, summary=SUMMARY) 113 | async def insert_user(params: Input = EXAMPLE, request: Request) -> Output: 114 | _, log = create_id_logger(request=request, endpoint=ENDPOINT) 115 | log.debug(f'received request for {request.url} with params {params}.') 116 | database = app.databases['userdb'].dbase 117 | async with database.transaction(): 118 | query = insert(User).values( 119 | email=params.email, 120 | password=params.password 121 | ) 122 | await database.execute(query) 123 | return Output() 124 | 125 | 126 | app/endpoints/v1/get_users.py 127 | ----------------------------- 128 | 129 | .. code-block:: python 130 | :caption: ``app/endpoints/v1/get_users.py`` 131 | 132 | from fastapi import Body 133 | from fastapi import APIRouter 134 | from fastapi_serviceutils.app import Endpoint 135 | from fastapi_serviceutils.app import create_id_logger 136 | 137 | from app.endpoints.v1.dbs import User 138 | from app.endpoints.v1.models import User as Output 139 | 140 | ENDPOINT = Endpoint(router=APIRouter(), route='/get_users', version='v1') 141 | SUMMARY = 'Example request.' 142 | 143 | @ENDPOINT.router.post('/', response_model=Output, summary=SUMMARY) 144 | async def get_users(request: Request) -> List[Output]: 145 | _, log = create_id_logger(request=request, endpoint=ENDPOINT) 146 | log.debug(f'received request for {request.url}.') 147 | database = app.databases['userdb'].dbase 148 | async with database.transaction(): 149 | users = await database.fetch_all(User.__table__.select()) 150 | return users 151 | 152 | 153 | app/endpoints/v1/\__init\__.py 154 | ------------------------------- 155 | 156 | Finally we include these endpoints to our ``ENDPOINTS``. 157 | 158 | .. code-block:: python 159 | :caption: ``__init__.py`` 160 | 161 | from fastapi_serviceutils.endpoints import set_version_endpoints 162 | 163 | from app.endpoints.v1 import get_users 164 | from app.endpoints.v1 import insert_user 165 | 166 | ENDPOINTS = set_version_endpoints( 167 | endpoints=[get_users, insert_user], 168 | version='v1', 169 | prefix_template='/api/{version}{route}' 170 | ) 171 | 172 | __all__ = ['ENDPOINTS'] 173 | 174 | 175 | The rest of our service, like the ``main.py``, the ``__init__.py`` files of 176 | the modules, etc. have the same content as described in ``exampleservice``. 177 | -------------------------------------------------------------------------------- /docs/external_resources_services.rst: -------------------------------------------------------------------------------- 1 | Services 2 | ======== 3 | 4 | If we need to call external services we first have to declare the service 5 | inside the ``config.yml`` like the following: 6 | 7 | .. code-block:: yaml 8 | :caption: ``app/config.yml`` 9 | 10 | ... 11 | external_resources: 12 | services: 13 | testservice: 14 | url: http://someserviceurl:someport 15 | servicetype: rest 16 | databases: null 17 | other: null 18 | ... 19 | 20 | 21 | .. code-block:: python 22 | :caption: ``app/endpoints/v1/models.py`` 23 | 24 | from pydantic import BaseModel 25 | 26 | class CallExternalService(BaseModel): 27 | street: str 28 | street_number: str 29 | zip_code: str 30 | city: str 31 | country: str 32 | 33 | class ExternalServiceResult(BaseModel): 34 | longitude: str 35 | latitude: str 36 | 37 | 38 | .. code-block:: python 39 | :caption: ``app/endpoints/v1/external_service.py`` 40 | 41 | from fastapi import APIRouter 42 | from fastapi import Body 43 | from fastapi_serviceutils.app import Endpoint 44 | from fastapi_serviceutils.app import create_id_logger 45 | from fastapi_serviceutils.utils.external_resources.services import call_service 46 | from starlette.requests import Request 47 | 48 | from app.endpoints.v1.models import CallExternalService as Input 49 | from app.endpoints.v1.models import ExternalServiceResult as Output 50 | 51 | ENDPOINT = Endpoint(router=APIRouter(), route='/use_service', version='v1') 52 | SUMMARY = 'Example request using an external service.' 53 | EXAMPLE = Body( 54 | ..., 55 | example={ 56 | 'street': 'anystreetname', 57 | 'street_number': '42', 58 | 'city': 'anycity', 59 | 'country': 'gallifrey' 60 | } 61 | ) 62 | 63 | @ENDPOINT.router.post('/', response_model=Output, summary=SUMMARY) 64 | async def use_service(params: Input = EXAMPLE, request: Request) -> Output: 65 | data_to_fetch = { 66 | 'street': params.street, 67 | 'auth_key': 'fnbkjgkegej', 68 | 'street_number': params.street_number, 69 | 'city': params.city, 70 | 'country': params.country 71 | } 72 | return await call_service( 73 | url=app.databases['testservice'].url, 74 | params=data_to_fetch, 75 | model=ExternalServiceResult 76 | ) 77 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. include:: sources.rst 2 | 3 | 4 | Getting Started 5 | --------------- 6 | After cloning the repository the development environment can be initialized 7 | using: 8 | 9 | .. code-block:: bash 10 | 11 | make init 12 | 13 | This will create the dev environment ``fastapi_serviceutils/dev``. 14 | Activate it. 15 | 16 | .. note:: 17 | 18 | Make sure to always activate the environment when you start 19 | working on the project in a new terminal using 20 | 21 | .. code-block:: bash 22 | 23 | poetry shell 24 | 25 | 26 | To update dependencies and ``poetry.lock``: 27 | 28 | .. code-block:: bash 29 | 30 | make update 31 | 32 | This also creates ``requirements.txt`` to be used for Docker_. 33 | -------------------------------------------------------------------------------- /docs/helpers.rst: -------------------------------------------------------------------------------- 1 | Helpers 2 | ------- 3 | 4 | 5 | .. include:: create_service.rst 6 | 7 | .. include:: makefile.rst 8 | 9 | .. include:: tmuxp.rst 10 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | fastapi-serviceutils 2 | ==================== 3 | 4 | .. include:: sources.rst 5 | 6 | .. image:: https://img.shields.io/badge/python-3.7-green.svg 7 | :target: https://img.shields.io/badge/python-3.7-green.svg 8 | :alt: Python 3.7 9 | 10 | .. image:: _static/coverage.svg 11 | :target: _static/coverage.svg 12 | :alt: coverage 13 | 14 | .. image:: https://img.shields.io/badge/License-MIT-blue.svg 15 | :target: https://img.shields.io/badge/License-MIT-blue.svg 16 | :alt: License MIT 17 | 18 | .. image:: _static/dephell.svg 19 | :target: https://github.com/dephell 20 | :alt: powered by dephell 21 | 22 | .. image:: _static/logo.png 23 | :target: _static/logo.png 24 | :alt: logo 25 | :width: 300 26 | :align: center 27 | 28 | 29 | Services stand for **portability** and **scalability**, so the **deployment** 30 | and **configuration** of these service should be as easy as possible. 31 | To achieve this a service based on fastapi-serviceutils is configured using 32 | a ``config.yml``. 33 | These settings can be overwritten using **environment-variables**. 34 | Dependency management for these services is generalized using a combination of 35 | Dephell_ and Poetry_. 36 | 37 | For monitoring and chaining of service-calls some default endpoints should 38 | always be defined. 39 | For example an endpoint to check if the service is alive (``/api/alive``) and 40 | an endpoint to access the config of the service (``/api/config``). 41 | These endpoints are automatically added in services using fastapi-serviceutils 42 | if defined in the ``config.yml`` of the service. 43 | 44 | Because a service should focus on only one task it may be required to create 45 | multiple small services in a short time. 46 | As always time matters. 47 | For this fastapi-serviceutils allows ** fast creation of new services** with 48 | ``create_service``. 49 | 50 | If an error occurs during a service-call it is important to have **detailed 51 | logs** with a **good traceback**. 52 | To achieve this the default logging of fastapi is optimized in 53 | fastapi-serviceutils using ``loguru``. 54 | 55 | Fastapi allows easily created **swagger-documentation** for service-endpoints. 56 | This is optimal for clients wanting to integrate these endpoints. 57 | For developers of the service an additional **apidoc-documentation** of the 58 | service and the source-code is required (most popular are documentations 59 | created using Sphinx_ or MKDocs). 60 | Fastapi-serviceutils based services **serve sphinx-based documentations** using 61 | **google-documentation style** in the code and rst-files inside the 62 | docs-folder. 63 | 64 | The development of these services should be as much generalized as possible for 65 | easy workflows, as less manual steps as possible for the developer and short 66 | onboarding times. 67 | For this fastapi-serviceutils includes a Makefile_ for most common tasks 68 | during development. 69 | There is also a Tmuxp_-config file to create a tmux-session for development. 70 | 71 | 72 | Features 73 | -------- 74 | 75 | * **optimized logging** using Loguru_ 76 | * **optimized exception handling** by additional exception handler 77 | ``log_exception handler`` 78 | * usage of a **config.yml**-file to configure the service 79 | * usage of **environment-variables** (`Environment variable`_ overwrites 80 | config-value) to configure the service 81 | * easily **serve the apidoc** with the service 82 | * easy deploment using Docker_ combined with `Docker compose`_ 83 | * fast creation of new service with :doc:`/create_service` 84 | * Makefile_ and Tmuxp_-config for easier development of services based on 85 | fastapi-serviceutils using **Make** and **tmux-session** 86 | 87 | 88 | Content 89 | ------- 90 | 91 | Fastapi-serviceutils contains three subpackages: 92 | 93 | * :mod:`fastapi_serviceutils.app` 94 | * :mod:`fastapi_serviceutils.cli` 95 | * :mod:`fastapi_serviceutils.utils` 96 | 97 | ``fastapi_serviceutils.app`` contains functions and classes for 98 | app-configuration (like config.yml file, logger, etc.), handlers and endpoint 99 | creation. 100 | 101 | ``fastapi_serviceutils.cli`` contains executables for easier development like 102 | ``create_service`` to use the fastapi_serviceutils_template. 103 | 104 | ``fastapi_serviceutils.utils`` contain utils to interact with external 105 | resources like databases and services, testutils and other utilities. 106 | 107 | To see detailed usage of these functions and classes, and also recommended 108 | service-structure, see :ref:`exampleservice`. 109 | 110 | Table of Contents 111 | ----------------- 112 | 113 | .. toctree:: 114 | :maxdepth: 2 115 | :glob: 116 | 117 | usage.rst 118 | development.rst 119 | Code-documentation 120 | see_also.rst 121 | -------------------------------------------------------------------------------- /docs/makefile.rst: -------------------------------------------------------------------------------- 1 | Makefile 2 | ^^^^^^^^ 3 | 4 | Usual tasks during development are wrapped inside the Makefile. 5 | This contains updating of the environment, creation of the docs, etc. 6 | 7 | .. literalinclude:: makefile_help.txt 8 | :start-after: make[1] 9 | :end-before: make[1] 10 | -------------------------------------------------------------------------------- /docs/makefile_help.txt: -------------------------------------------------------------------------------- 1 | make[1]: Entering directory '/home/skallfass/devl/services/fastapi/fastapi_serviceutils' 2 | Helpers for development of fastapi_serviceutils. 3 | 4 | Usage: 5 | 6 | make [flags...] 7 | 8 | Targets: 9 | 10 | check Run all checks defined in .pre-commit-config.yaml. 11 | clean Clean the working directory from temporary files and caches. 12 | doc Create sphinx documentation for the project. 13 | docs Create sphinx documentation for the project. 14 | finalize Finalize the main env. 15 | help Show the help prompt. 16 | info Show info about current project. 17 | init Initialize project 18 | tests Run tests using pytest. 19 | update Update environments based on pyproject.toml definitions. 20 | 21 | Flags: 22 | 23 | 24 | 25 | Note: 26 | This workflow requires the following programs / tools to be installed: 27 | - poetry 28 | - dephell 29 | - pyenv 30 | make[1]: Leaving directory '/home/skallfass/devl/services/fastapi/fastapi_serviceutils' 31 | -------------------------------------------------------------------------------- /docs/see_also.rst: -------------------------------------------------------------------------------- 1 | See also 2 | ======== 3 | 4 | .. include:: sources.rst 5 | 6 | Internal documentation: 7 | 8 | * :doc:`API Documentation ` 9 | * :doc:`Development ` 10 | * :doc:`Deployment ` 11 | 12 | Used tools: 13 | 14 | * Cookiecutter_ 15 | * Dephell_ 16 | * Docker_ 17 | * `Docker compose`_ 18 | * Make_ 19 | * Poetry_ 20 | * restructuredText_ 21 | * Sphinx_ 22 | * Tmux_ 23 | * Tmuxp_ 24 | 25 | Used packages: 26 | 27 | * Databases_ 28 | * FastAPI_ 29 | * Loguru_ 30 | * Requests_ 31 | * Toolz_ 32 | * SQLAlchemy_ 33 | 34 | Additional sources: 35 | 36 | * `FastAPI deployment`_ 37 | * `Google docstring standard`_ 38 | * `reStructuredText reference`_ 39 | * `Type Annotations`_ 40 | -------------------------------------------------------------------------------- /docs/sources.rst: -------------------------------------------------------------------------------- 1 | .. _Cookiecutter: https://cookiecutter.readthedocs.io/en/latest/ 2 | .. _Databases: https://www.encode.io/databases/ 3 | .. _Dephell: https://dephell.org/docs/ 4 | .. _Docker: https://docs.docker.com/ 5 | .. _Docker compose: https://docs.docker.com/compose/ 6 | .. _Docker compose file: https://docs.docker.com/compose/compose-file/ 7 | .. _Dockerfile: https://docs.docker.com/engine/reference/builder/ 8 | .. _Environment variable: https://en.wikipedia.org/wiki/Environment_variable 9 | .. _FastAPI: https://fastapi.tiangolo.com/ 10 | .. _FastAPI deployment: https://fastapi.tiangolo.com/deployment/ 11 | .. _Google docstring standard: http://www.sphinx-doc.org/en/master/usage/extensions/example_google.html 12 | .. _Loguru: https://loguru.readthedocs.io/en/stable/index.html 13 | .. _Make: https://www.gnu.org/software/make/manual/make.html 14 | .. _Makefile: https://www.gnu.org/software/make/manual/make.html 15 | .. _Poetry: https://poetry.eustace.io/docs/ 16 | .. _Pytest: https://pytest.org/en/latest/ 17 | .. _Requests: https://requests.kennethreitz.org/en/master/ 18 | .. _restructuredText: http://docutils.sourceforge.net/rst.html 19 | .. _reStructuredText reference: http://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html 20 | .. _Semver: https://semver.org/ 21 | .. _Sphinx: http://www.sphinx-doc.org 22 | .. _SQLAlchemy: https://www.sqlalchemy.org/ 23 | .. _Tmux: https://github.com/tmux/tmux/wiki 24 | .. _Tmuxp: https://tmuxp.git-pull.com/en/latest/ 25 | .. _Toolz: https://toolz.readthedocs.io/en/latest/ 26 | .. _Type Annotations: https://docs.python.org/3/library/typing.html 27 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | .. include:: sources.rst 2 | 3 | 4 | Testing 5 | ------- 6 | 7 | All tests are located inside the folder ``tests``. 8 | Tests for a module should be names like ``_test.py``. 9 | 10 | .. Note:: 11 | 12 | For often used functions and workflows during testing the functions and 13 | classes inside :mod:`fastapi_serviceutils.utils.tests` can be used. 14 | 15 | To run the tests run: 16 | 17 | .. code-block:: bash 18 | 19 | make tests 20 | 21 | A HTML coverage report is automatically created in the ``htmlcov`` directory. 22 | 23 | .. seealso:: 24 | 25 | For additional information how to test fastapi-applications: 26 | 27 | * https://fastapi.tiangolo.com/tutorial/testing/ 28 | * https://fastapi.tiangolo.com/tutorial/testing-dependencies/ 29 | 30 | For information how to test async functions: 31 | 32 | * https://github.com/pytest-dev/pytest-asyncio 33 | -------------------------------------------------------------------------------- /docs/tmuxp.rst: -------------------------------------------------------------------------------- 1 | tmuxp 2 | ^^^^^ 3 | 4 | .. include:: sources.rst 5 | 6 | For a predefined development environment the ``.tmuxp.yml`` configuration can 7 | be used to create a Tmux_-session (using Tmuxp_) with a window including three 8 | panels: 9 | 10 | * one panel for **editing files** 11 | * one panel **running the service** 12 | * one panel **running the tests** 13 | 14 | Run the following command to create the tmux-session: 15 | 16 | .. code-block:: bash 17 | 18 | tmuxp load . 19 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | exampleservice.rst 8 | external_resources.rst 9 | helpers.rst 10 | deployment.rst 11 | -------------------------------------------------------------------------------- /fastapi_serviceutils/__init__.py: -------------------------------------------------------------------------------- 1 | """Contain utils for fastapi based services. 2 | 3 | This contains: 4 | 5 | * optimized logging using Loguru 6 | * optimized exception handling by additional exception handler 7 | log_exception handler 8 | * usage of a config.yml-file to configure the service 9 | * usage of environment-variables (Environment variable overwrites config-value) 10 | to configure the service 11 | * easily serve the apidoc with the service 12 | * easy deploment using Docker combined with Docker compose 13 | * fast creation of new service with create_service 14 | * Makefile and Tmuxp-config for easier development of services based on 15 | fastapi-serviceutils using Make and tmux-session 16 | 17 | This module defines the function :func:`make_app` to create an app containing 18 | the above mentioned features. 19 | """ 20 | from pathlib import Path 21 | from typing import List 22 | 23 | from fastapi import FastAPI 24 | from starlette.middleware.trustedhost import TrustedHostMiddleware 25 | 26 | from .app import collect_config_definition 27 | from .app import Config 28 | from .app import customize_logging 29 | from .app import Endpoint 30 | from .app import update_config 31 | from .app.endpoints import add_default_endpoints 32 | from .app.handlers import log_exception_handler 33 | from .utils.docs import mount_apidoc 34 | from .utils.external_resources.dbs import add_databases_to_app 35 | from .utils.external_resources.services import add_services_to_app 36 | 37 | __version__ = '2.1.0' 38 | 39 | 40 | def include_endpoints_and_middlewares_to_app( 41 | app: FastAPI, 42 | endpoints: List[Endpoint], 43 | enable_middlewares: List[str], 44 | additional_middlewares: list 45 | ) -> FastAPI: 46 | """Include endpoints and middlewares (and defaults) to app. 47 | 48 | Note: 49 | :func:`log_exception_handler` is not really a middleware but if 50 | it should be enabled, ``'log_exception'`` should be included in 51 | ``enable_middlewares``. 52 | 53 | Note: 54 | A router included by this function has additional attributes: 55 | 56 | * ``router.mode`` 57 | * ``router.config`` 58 | * ``router.logger`` 59 | * ``router.databases`` 60 | * ``router.services`` 61 | 62 | Parameters: 63 | app: the app to add the endpoints and middlewares. 64 | endpoints: the endpoints to include into the app. 65 | enable_middlewares: the default middlewares to enable. 66 | additional_middlewares: additional middlewares to add to the app. 67 | 68 | Returns: 69 | the modified app containing the endpoints, middlewares and handlers. 70 | 71 | """ 72 | # iterate over endpoints, define additional attributes required inside the 73 | # endpoints and include the endpoint to the router of the app. 74 | for endpoint in endpoints: 75 | endpoint.router.mode = app.mode 76 | endpoint.router.config = app.config 77 | endpoint.router.logger = app.logger 78 | endpoint.router.databases = app.databases 79 | endpoint.router.services = app.services 80 | if endpoint.tags: 81 | app.include_router( 82 | endpoint.router, 83 | prefix=endpoint.route, 84 | tags=endpoint.tags 85 | ) 86 | else: 87 | app.include_router(endpoint.router, prefix=endpoint.route) 88 | 89 | # add the apidoc to app 90 | if app.config.service.apidoc_dir: 91 | mount_apidoc(app=app) 92 | 93 | # add middleware to be able to limit access to service to specific hosts 94 | if 'trusted_hosts' in enable_middlewares: 95 | app.add_middleware( 96 | TrustedHostMiddleware, 97 | allowed_hosts=app.config.service.allowed_hosts 98 | ) 99 | 100 | # add custom exception handler to log exceptions 101 | if 'log_exception' in enable_middlewares: 102 | app.exception_handler(Exception)(log_exception_handler) 103 | 104 | # add additional defined middlewares 105 | # TODO: in future releases the addition of more complex middlewares should 106 | # be possible 107 | for middleware in additional_middlewares: 108 | app.add_middleware(middleware) 109 | 110 | return app 111 | 112 | 113 | def make_app( 114 | config_path: Path, 115 | version: str, 116 | endpoints: List[Endpoint], 117 | enable_middlewares: List[str], 118 | additional_middlewares: list, 119 | ) -> FastAPI: 120 | """Create app with endpoints and middlewares. 121 | 122 | App is configured using the config of the service and defined 123 | environment-variables. 124 | Also logger is configured and default endpoints and additional endpoints 125 | added. 126 | Same for middlewares. 127 | 128 | Note: 129 | An app created by this function has additional attributes: 130 | 131 | * ``app.logger`` 132 | * ``app.config`` 133 | * ``app.databases`` 134 | * ``app.services`` 135 | * ``app.mode`` 136 | 137 | Parameters: 138 | config_path: the path for the config file to use for the app. 139 | version: current version of the service, should be ``__version__`` 140 | variable inside the module ``app`` of your service. 141 | endpoints: the endpoints to include to the app. 142 | enable_middlewares: list of the middlewares to add. 143 | additional_middlewares: list of non default middlewares to add to the 144 | app. 145 | 146 | Returns: 147 | the created app. 148 | 149 | """ 150 | # load the config and environment-variables for the service and 151 | # combine these information to initialize the app 152 | config = collect_config_definition(config_path=config_path) 153 | 154 | # update mode and logger-definitions with environment-variables if defined 155 | # ATTENTION: the environment-variable has the prefix of the servicename 156 | config = update_config( 157 | env_vars=config.available_environment_variables.env_vars, 158 | external_resources_env_vars=( 159 | config.available_environment_variables.external_resources_env_vars 160 | ), 161 | rules_env_vars=config.available_environment_variables.rules_env_vars, 162 | config=config, 163 | model=Config, 164 | ) 165 | 166 | # convert config-attribute types if necessary 167 | config.logger.path = Path(config.logger.path) 168 | config.service.readme = Path(config.service.readme) 169 | 170 | # initialize the app, add combined configuration, mode and logger 171 | app = FastAPI( 172 | title=f'{config.service.name} [{config.service.mode.upper()}]', 173 | description=config.service.readme.read_text(), 174 | version=version, 175 | ) 176 | 177 | # add additional attributes to the app like the config and the runtime-mode 178 | app.config = config 179 | app.mode = config.service.mode 180 | 181 | # Set the logging-configuration 182 | app.logger = customize_logging( 183 | config.logger.path / config.logger.filename.format(mode=app.mode), 184 | level=config.logger.level, 185 | retention=config.logger.retention, 186 | rotation=config.logger.rotation, 187 | _format=config.logger.format 188 | ) 189 | 190 | # if dependencies for external databases are defined in the config, add 191 | # these database-definitions to the app 192 | if config.external_resources.databases: 193 | app = add_databases_to_app( 194 | app, 195 | dbs=config.external_resources.databases 196 | ) 197 | else: 198 | app.databases = {} 199 | 200 | # if dependencies for external services are defined in the config, add 201 | # these service-definitions to the app 202 | if config.external_resources.services: 203 | app = add_services_to_app( 204 | app, 205 | services=config.external_resources.services 206 | ) 207 | else: 208 | app.services = {} 209 | 210 | # add default endpoints if defined in the config 211 | endpoints = add_default_endpoints(endpoints=endpoints, config=app.config) 212 | 213 | # include defined routers and middlewares to the app 214 | return include_endpoints_and_middlewares_to_app( 215 | app=app, 216 | endpoints=endpoints, 217 | enable_middlewares=enable_middlewares, 218 | additional_middlewares=additional_middlewares 219 | ) 220 | 221 | 222 | __all__ = ['make_app', 'include_endpoints_and_middlewares_to_app'] 223 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/__init__.py: -------------------------------------------------------------------------------- 1 | """Helpers for fastapi-app especially the endpoint-functions. 2 | 3 | Inside endpoints defined using fastapi-serviceutils the variable ``ENDPOINTS`` 4 | should be defined as an instance of :class:`Endpoint`. 5 | 6 | For better traceback inside the logs :func:`create_id_logger` is used inside 7 | the endpoint-function. 8 | """ 9 | from dataclasses import dataclass 10 | from typing import List 11 | 12 | from fastapi import APIRouter 13 | from loguru._logger import Logger 14 | from starlette.requests import Request 15 | 16 | from .logger import customize_logging 17 | from .service_config import collect_config_definition 18 | from .service_config import Config 19 | from .service_config import update_config 20 | 21 | 22 | @dataclass 23 | class Endpoint: 24 | """Endpoint as required inside each endpoint module. 25 | 26 | Attributes: 27 | router: the router for this endpoint. 28 | route: the route to this endpoint (is modified for the app using 29 | function 30 | :func:`fastapi_serviceutils.app.endpoints.set_version_endpoints`). 31 | tags: the tags for the endpoint (used for swagger-documentation). 32 | version: the version of the endpoint. 33 | 34 | """ 35 | router: APIRouter 36 | route: str 37 | tags: List[str] = None 38 | version: str = None 39 | 40 | 41 | def create_request_id(request: Request): 42 | """Create a request-id baed on the attributes of the passed request. 43 | 44 | Parameters: 45 | request: the request to create the id for. 46 | 47 | Returns: 48 | the request-id. 49 | 50 | """ 51 | return abs(hash(f'{request.client}{request.headers}{request.body}')) 52 | 53 | 54 | def create_id_logger( 55 | request: Request, 56 | endpoint: Endpoint, 57 | ) -> [int, 58 | Logger]: 59 | """Create the request-id and the request-specific logger. 60 | 61 | This function is meant to be used inside an endpoint-function to be able 62 | to use a unique request-id in each request for better traceback in the 63 | logs. 64 | 65 | Parameters: 66 | request: the request to create id and logger. 67 | endpoint: the endpoint to extract the logger-function. 68 | 69 | Returns: 70 | the request-id and the customized logger. 71 | 72 | """ 73 | request_id = create_request_id(request) 74 | log = endpoint.router.logger.bind(request_id=request_id) 75 | return request_id, log 76 | 77 | 78 | __all__ = [ 79 | 'collect_config_definition', 80 | 'Config', 81 | 'create_id_logger', 82 | 'create_request_id', 83 | 'customize_logging', 84 | 'update_config', 85 | ] 86 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | """Endpoint registration and configuration helpers. 2 | 3 | Also includes default endpoint handling. 4 | """ 5 | import copy 6 | from typing import List 7 | 8 | from fastapi_serviceutils.app import Endpoint 9 | from fastapi_serviceutils.app.endpoints.default import alive as alive_endpoint 10 | from fastapi_serviceutils.app.endpoints.default import config as config_endpoint 11 | from fastapi_serviceutils.app.service_config import Config 12 | 13 | 14 | def set_version_endpoints( 15 | endpoints: List[Endpoint], 16 | version: str, 17 | prefix_template: str, 18 | **kwargs: dict 19 | ) -> List[Endpoint]: 20 | """Configures the endpoints inside a version-module. 21 | 22 | Modify the route to the correct route using the ``route``-attribute of 23 | each endpoint and modify it using the passed prefix_template in 24 | combination with the passed kwargs. 25 | 26 | Also used to set the ``'latest'`` endpoints. 27 | 28 | Parameters: 29 | endpoints: the endpoints to set the version and correct route. 30 | version: the version to set for the endpoints. 31 | prefix_template: the template to use for the resulting route. 32 | kwargs: additional kwargs required by the prefix_template. As default 33 | the following keys inside kwargs are already set: 34 | 35 | * ``route`` 36 | * ``version`` 37 | 38 | So these can already be used inside the prefix_template. 39 | 40 | Returns: 41 | the resulting endpoints. 42 | 43 | """ 44 | version_endpoints = [] 45 | if not kwargs: 46 | kwargs = {} 47 | else: 48 | kwargs = copy.deepcopy(kwargs) 49 | 50 | for endpoint_definition in endpoints: 51 | if isinstance(endpoint_definition, Endpoint): 52 | endpoint = Endpoint( 53 | route=endpoint_definition.route, 54 | router=endpoint_definition.router, 55 | version=endpoint_definition.version 56 | ) 57 | else: 58 | endpoint = Endpoint( 59 | route=endpoint_definition.ENDPOINT.route, 60 | router=endpoint_definition.ENDPOINT.router, 61 | version=endpoint_definition.ENDPOINT.version 62 | ) 63 | endpoint.tags = [version] 64 | if version == 'latest': 65 | route = endpoint.route.replace( 66 | endpoint_definition.version, 67 | version 68 | ) 69 | else: 70 | route = endpoint.route 71 | kwargs.update({'route': route, 'version': version}) 72 | endpoint.version = version 73 | endpoint.route = prefix_template.format(**kwargs) 74 | version_endpoints.append(endpoint) 75 | return version_endpoints 76 | 77 | 78 | def add_default_endpoints(endpoints: List[Endpoint], 79 | config: Config) -> List[Endpoint]: 80 | """Add default endpoints to existing endpoints. 81 | 82 | Currently the following default-endpoints are available: 83 | 84 | * ``'alive`` 85 | * ``'config'`` 86 | 87 | Parameters: 88 | endpoints: the already set endpoints to add the default ones. 89 | config: the config of the service to extract the information which 90 | default-endpoints to add. 91 | 92 | Returns: 93 | the passed endpoints with added default endpoints. 94 | 95 | """ 96 | default_endpoints = { 97 | 'alive': alive_endpoint.ENDPOINT, 98 | 'config': config_endpoint.ENDPOINT 99 | } 100 | 101 | for endpoint in config.service.use_default_endpoints: 102 | current = default_endpoints[endpoint] 103 | current.tags = ['status'] 104 | endpoints.append(current) 105 | return endpoints 106 | 107 | 108 | __all__ = ['add_default_endpoints', 'set_version_endpoints'] 109 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/endpoints/default/__init__.py: -------------------------------------------------------------------------------- 1 | """Contain default endpoints and their models available as routes.""" 2 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/endpoints/default/alive.py: -------------------------------------------------------------------------------- 1 | """Endpoint to check if service is alive.""" 2 | from fastapi import APIRouter 3 | from starlette.requests import Request 4 | 5 | from fastapi_serviceutils.app import create_id_logger 6 | from fastapi_serviceutils.app import Endpoint 7 | from fastapi_serviceutils.app.endpoints.default.models import Alive 8 | 9 | ENDPOINT = Endpoint(router=APIRouter(), route='/api/alive') 10 | SUMMARY = 'Check if service is alive.' 11 | 12 | 13 | @ENDPOINT.router.post('/', response_model=Alive, summary=SUMMARY) 14 | async def alive(request: Request) -> Alive: 15 | """Check if service is alive. 16 | 17 | Returns: 18 | the information that the service is alive. 19 | 20 | """ 21 | _, log = create_id_logger(request=request, endpoint=ENDPOINT) 22 | log.debug(f'received request for endpoint {request.url}.') 23 | return Alive(alive=True) 24 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/endpoints/default/config.py: -------------------------------------------------------------------------------- 1 | """Endpoint to return currently used configuration of the service.""" 2 | from fastapi import APIRouter 3 | from starlette.requests import Request 4 | 5 | from fastapi_serviceutils.app import Config 6 | from fastapi_serviceutils.app import create_id_logger 7 | from fastapi_serviceutils.app import Endpoint 8 | 9 | ENDPOINT = Endpoint(router=APIRouter(), route='/api/config') 10 | SUMMARY = 'Get currently used config.' 11 | 12 | 13 | @ENDPOINT.router.post('/', response_model=Config, summary=SUMMARY) 14 | async def get_config(request: Request) -> Config: 15 | """Get currently used config of the service. 16 | 17 | Returns: 18 | the content of the currently used config. 19 | 20 | """ 21 | _, log = create_id_logger(request=request, endpoint=ENDPOINT) 22 | log.debug(f'received request for endpoint {request.url}.') 23 | return Config.parse_raw(ENDPOINT.router.config.json()) 24 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/endpoints/default/models.py: -------------------------------------------------------------------------------- 1 | """Contain not version depending models for default-endpoints of service. 2 | 3 | For each endpoint requiring parameters as input on call an input-model should 4 | be defined here. 5 | 6 | For each endpoint returning data on call an output-model should be defined 7 | here, too. 8 | 9 | Currently both default-endpoints do not require input-parameters, so no 10 | input-models are defined here yet. 11 | 12 | The config-endpoint returns data of model 13 | :class:`fastapi_serviceutils.app.service_config.Config` so this one is not 14 | defined here, because already defined. 15 | 16 | The alive-endpoint returns data of model :class:`Alive`. 17 | """ 18 | from pydantic import BaseModel 19 | 20 | 21 | class Alive(BaseModel): 22 | """Represent the alive-result of the endpoint ``/api/alive``.""" 23 | alive: bool 24 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | """Available handlers for services based on fastapi_serviceutils.""" 2 | import traceback 3 | from typing import Union 4 | 5 | from fastapi.exception_handlers import http_exception_handler 6 | from loguru import logger 7 | from starlette.exceptions import HTTPException as StarletteHTTPException 8 | from starlette.requests import Request 9 | from starlette.responses import Response 10 | 11 | from fastapi_serviceutils.app import create_request_id 12 | 13 | 14 | async def log_exception_handler( 15 | request: Request, 16 | exc: Union[StarletteHTTPException, 17 | Exception] 18 | ) -> Response: 19 | """Add log of exception if an exception occur.""" 20 | # add the request_id to the logger to be able to understand what request 21 | # caused the exception 22 | log = logger.bind(request_id=create_request_id(request)) 23 | 24 | if isinstance(exc, StarletteHTTPException): 25 | # do not log exception for wrong endpoints 26 | if exc.status_code != 404: 27 | log.error( 28 | 'following error occurred: {detail}. {error}'.format( 29 | detail=exc.detail, 30 | error=repr(exc) 31 | ) 32 | ) 33 | return await http_exception_handler(request, exc) 34 | 35 | # if not already a StarletteHTTPException, convert the exception to it 36 | _exception = StarletteHTTPException( 37 | status_code=500, 38 | detail=traceback.format_exc().split('\n') 39 | ) 40 | log.error( 41 | f'{_exception.detail}\n ' 42 | f'request from {request.client} to url {request.url}' 43 | ) 44 | return await http_exception_handler(request, _exception) 45 | 46 | 47 | __all__ = [ 48 | 'log_exception_handler', 49 | ] 50 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/logger.py: -------------------------------------------------------------------------------- 1 | """Contain helpers to customize logging for the service.""" 2 | import logging 3 | import sys 4 | from pathlib import Path 5 | 6 | from loguru import logger 7 | 8 | 9 | class _InterceptHandler(logging.Handler): 10 | loglevel_mapping = { 11 | 50: 'CRITICAL', 12 | 40: 'ERROR', 13 | 30: 'WARNING', 14 | 20: 'INFO', 15 | 10: 'DEBUG', 16 | 0: 'NOTSET', 17 | } 18 | 19 | def emit(self, record): 20 | # Get corresponding Loguru level if it exists 21 | try: 22 | level = logger.level(record.levelname).name 23 | except AttributeError: 24 | level = self.loglevel_mapping[record.levelno] 25 | 26 | # Find caller from where originated the logging call 27 | frame, depth = logging.currentframe(), 2 28 | while frame.f_code.co_filename == logging.__file__: 29 | frame = frame.f_back 30 | depth += 1 31 | 32 | log = logger.bind(request_id='app') 33 | log.opt( 34 | depth=depth, 35 | exception=record.exc_info 36 | ).log(level, 37 | record.getMessage()) 38 | 39 | 40 | def customize_logging( 41 | filepath: Path, 42 | level: str, 43 | rotation: str, 44 | retention: str, 45 | _format: str 46 | ): 47 | """Define the logger to be used by the service based on loguru. 48 | 49 | Parameters: 50 | filepath: the path where to store the logfiles. 51 | level: the minimum log-level to log. 52 | rotation: when to rotate the logfile. 53 | retention: when to remove logfiles. 54 | _format: the logformat to use. 55 | 56 | Returns: 57 | the logger to be used by the service. 58 | 59 | """ 60 | filepath.parent.mkdir(parents=True, exist_ok=True) 61 | 62 | logger.remove() 63 | logger.add( 64 | sys.stdout, 65 | enqueue=True, 66 | backtrace=True, 67 | level=level.upper(), 68 | format=_format 69 | ) 70 | logger.add( 71 | str(filepath), 72 | rotation=rotation, 73 | retention=retention, 74 | enqueue=True, 75 | backtrace=True, 76 | level=level.upper(), 77 | format=_format 78 | ) 79 | logging.basicConfig(handlers=[_InterceptHandler()], level=0) 80 | for _log in ['uvicorn', 81 | 'uvicorn.error', 82 | 'fastapi', 83 | 'sqlalchemy', 84 | 'databases']: 85 | _logger = logging.getLogger(_log) 86 | _logger.handlers = [_InterceptHandler()] 87 | 88 | return logger.bind(request_id='app') 89 | 90 | 91 | __all__ = [ 92 | 'customize_logging', 93 | ] 94 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | """Middlewares available for fastapi- / starlette-based services.""" 2 | -------------------------------------------------------------------------------- /fastapi_serviceutils/app/service_config.py: -------------------------------------------------------------------------------- 1 | """Collect config for service and convert to instance of :class:`Config`.""" 2 | import copy 3 | import os 4 | from pathlib import Path 5 | from typing import Dict 6 | from typing import List 7 | from typing import NoReturn 8 | from typing import Union 9 | 10 | import yaml 11 | from pydantic import BaseModel 12 | from pydantic.dataclasses import dataclass 13 | from toolz.dicttoolz import update_in 14 | 15 | from fastapi_serviceutils.utils.external_resources.dbs import DatabaseDefinition 16 | from fastapi_serviceutils.utils.external_resources.services import ServiceDefinition 17 | 18 | 19 | class AvailableEnvironmentVariables(BaseModel): 20 | """Represent section of available environment variables in config.""" 21 | env_vars: List[str] 22 | external_resources_env_vars: List[str] 23 | rules_env_vars: List[str] 24 | 25 | 26 | class ServiceConfig(BaseModel): 27 | """Represent configuration of the service inside the config. 28 | 29 | Attributes: 30 | name: the name of the service. 31 | mode: the runtime-mode of the service. 32 | port: port to use for service. 33 | description: short description of the service. 34 | documentation_dir: path where the documentation dir for the apidoc 35 | is located. 36 | readme: path to the readme-file to integrate into the 37 | swagger-documentation. 38 | 39 | """ 40 | name: str 41 | mode: str 42 | port: int 43 | description: str 44 | apidoc_dir: str 45 | readme: str 46 | allowed_hosts: List[str] 47 | use_default_endpoints: List[str] 48 | 49 | 50 | class LoggerConfig(BaseModel): 51 | """Represent configuration of logger inside the config. 52 | 53 | Attributes: 54 | path: folder where the log-files should be saved. 55 | filename: the name of the log-file to use. 56 | level: the minimum log-level to log. 57 | rotation: when to rotate the log-file. 58 | retention: how long to keep log-files. 59 | format: log-format to use. 60 | 61 | """ 62 | path: str 63 | filename: str 64 | level: str 65 | rotation: str 66 | retention: str 67 | format: str 68 | 69 | 70 | @dataclass 71 | class ExternalResources: 72 | """Represent the definition of external-resources inside the config.""" 73 | databases: Dict[str, DatabaseDefinition] = None 74 | services: Dict[str, ServiceDefinition] = None 75 | other: dict = None 76 | 77 | def __post_init__(self) -> NoReturn: 78 | if self.databases: 79 | for name, attributes in self.databases.items(): 80 | self.databases.update( 81 | { 82 | name: 83 | DatabaseDefinition( 84 | name=name, 85 | dsn=attributes['dsn'], 86 | databasetype=attributes['databasetype'] 87 | ) 88 | } 89 | ) 90 | if self.services: 91 | for name, attributes in self.services.items(): 92 | self.services.update( 93 | { 94 | name: 95 | ServiceDefinition( 96 | name=name, 97 | url=attributes['url'], 98 | servicetype=attributes['servicetype'] 99 | ) 100 | } 101 | ) 102 | 103 | def __post_init_post_parse__(self): 104 | object.__delattr__(self, '__initialised__') 105 | 106 | 107 | class Config(BaseModel): 108 | """Represent config-content to configure service and its components. 109 | 110 | Attributes: 111 | service: general information about service, like name, where to find 112 | the readme, documentation-dir, etc. 113 | logger: configuration for the logger of the service. 114 | external_resources: if service depends on external-resources, 115 | this includes for example the url of such a dependency, etc. 116 | rules: special rules for the service. 117 | 118 | """ 119 | service: ServiceConfig 120 | logger: LoggerConfig 121 | available_environment_variables: AvailableEnvironmentVariables 122 | external_resources: ExternalResources 123 | rules: dict = None 124 | 125 | 126 | def collect_config_definition(config_path: Path) -> Config: 127 | """Collect the config for the service. 128 | 129 | Then convert its content to an instance of :class:`Config`. 130 | 131 | Parameters: 132 | config_path: the path of the config-file to use. 133 | 134 | Returns: 135 | the content of the config-file converted to an instance of 136 | :class:`Config`. 137 | 138 | """ 139 | return Config(**yaml.safe_load(config_path.read_text())) 140 | 141 | 142 | def _update_value_in_nested_dict_by_keylist( 143 | dictionary: dict, 144 | key_list: List[str], 145 | new_value 146 | ) -> dict: 147 | """Update the value of a nested-dictionary by a keylist with new value. 148 | 149 | Wrapper around :func:`toolz.dicttoolz.update_in`. 150 | 151 | Note: 152 | Do not update the original dict, returns a new dict with same content 153 | as original dict, but with update and required location. 154 | 155 | Parameters: 156 | dictionary: the dictionary to update. 157 | key_list: list of subkeys where to update the dictionary. 158 | new_value: the new value to update to. 159 | 160 | Returns: 161 | the updated dictionary. 162 | 163 | """ 164 | return update_in(dictionary, key_list, lambda x: new_value) 165 | 166 | 167 | def _use_environment_variable_for_variable( 168 | config: Union[Config, 169 | dict], 170 | keys: List[str], 171 | model: BaseModel, 172 | content_env_var: Union[str, 173 | int, 174 | float, 175 | None], 176 | ) -> Union[BaseModel, 177 | dict]: 178 | """Overwrite config with value of environment-variable. 179 | 180 | To be able to overwrite the config it must be converted to a dict (if it is 181 | not already). After updating the value it has to be converted back to an 182 | instance of the passed ``model``. 183 | 184 | Parameters: 185 | config: the config to update. 186 | keys: sublevels for the config to set the value. 187 | model: the model to convert back the config after update. 188 | content_env_var: the content of the environment-variable. 189 | env_var_name: the name of the variable. 190 | 191 | Returns: 192 | the updated config. 193 | 194 | """ 195 | if isinstance(config, dict): 196 | temp_config = copy.deepcopy(config) 197 | else: 198 | temp_config = copy.deepcopy(config.dict()) 199 | 200 | temp_config = _update_value_in_nested_dict_by_keylist( 201 | dictionary=temp_config, 202 | key_list=keys, 203 | new_value=content_env_var 204 | ) 205 | 206 | if model: 207 | return model.parse_obj(temp_config) 208 | 209 | return temp_config 210 | 211 | 212 | def _update_config_with_environment_variables( 213 | servicename: str, 214 | environment_variable_names: List[str], 215 | config: Union[Config, 216 | dict], 217 | model: BaseModel = None 218 | ) -> Union[BaseModel, 219 | dict]: 220 | """Update the config if environment-variables exist. 221 | 222 | If an environment variable exist, overwrite the value in the config with 223 | the value of the environment-variable. 224 | 225 | Finally store into ``info`` if original config-value is used or 226 | environment-variable. 227 | 228 | Parameters: 229 | servicename: the name of the service. 230 | environment_variable_name: the name of the environment-variable to 231 | check and use if set. 232 | config: the config containing the value to use / the config to 233 | overwrite with the value of the environment-variable. 234 | model: the model of the config to use. 235 | 236 | Returns: 237 | the updated config. 238 | 239 | """ 240 | for environment_variable_name in environment_variable_names: 241 | env_var_name = f'{servicename}_{environment_variable_name}' 242 | keys = env_var_name.replace(f'{servicename}_', '').lower().split('__') 243 | content_env_var = os.environ.get(env_var_name) 244 | 245 | if content_env_var: 246 | # overwrite config with the value of the environment-variable 247 | config = _use_environment_variable_for_variable( 248 | config=config, 249 | keys=keys, 250 | model=model, 251 | content_env_var=content_env_var, 252 | ) 253 | return config 254 | 255 | 256 | def update_config( 257 | config: Config, 258 | model: BaseModel, 259 | env_vars: List[str] = None, 260 | external_resources_env_vars: List[str] = None, 261 | rules_env_vars: List[str] = None, 262 | ) -> Config: 263 | """Update config with environment-variables if defined. 264 | 265 | Parameters: 266 | config: the config to update. 267 | model: the model of the config. 268 | env_vars: the environment-variables to use. 269 | external_resources_env_vars: the environment-variables for 270 | external-resource configuration. 271 | rules_env_vars: the environment-variables for rules configuration. 272 | 273 | Returns: 274 | the updated config. 275 | 276 | """ 277 | servicename = config.service.name.upper() 278 | # update mode and logger-definitions with environment-variables if defined 279 | # ATTENTION: the environment-variable has the prefix of the servicename 280 | if env_vars: 281 | config = _update_config_with_environment_variables( 282 | servicename=servicename, 283 | environment_variable_names=env_vars, 284 | config=config, 285 | model=model, 286 | ) 287 | 288 | # load special environment variables which can't be accessed by converting 289 | # the config to a dict. 290 | # ATTENTION: like above the environment-variable has the prefix of the 291 | # servicename 292 | if external_resources_env_vars: 293 | config.external_resources = _update_config_with_environment_variables( 294 | servicename=servicename, 295 | environment_variable_names=external_resources_env_vars, 296 | config=config.external_resources, 297 | ) 298 | 299 | # load special environment variables which can't be accessed by converting 300 | # the config to a dict. 301 | # ATTENTION: like above the environment-variable has the prefix of the 302 | # servicename 303 | if rules_env_vars: 304 | config.rules = _update_config_with_environment_variables( 305 | servicename=servicename, 306 | environment_variable_names=rules_env_vars, 307 | config=config.rules, 308 | ) 309 | return config 310 | 311 | 312 | __all__ = [ 313 | 'collect_config_definition', 314 | 'Config', 315 | 'update_config', 316 | ] 317 | -------------------------------------------------------------------------------- /fastapi_serviceutils/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """Helpers for fastapi-serviceutils based services. 2 | 3 | Modules and functions defined in this subpackage are meant to be used after 4 | installing fastapi-serviceutils using the entrypoints for these functions. 5 | 6 | For example to create a new service run the following command in shell: 7 | 8 | .. code:: bash 9 | 10 | create_service --help 11 | """ 12 | -------------------------------------------------------------------------------- /fastapi_serviceutils/cli/create_service.py: -------------------------------------------------------------------------------- 1 | """Create new service using the fastapi_serviceutils-template. 2 | 3 | For this functionality we use Cookiecutter. 4 | """ 5 | import sys 6 | from argparse import ArgumentParser 7 | from argparse import Namespace 8 | from pathlib import Path 9 | from string import ascii_letters 10 | from string import digits 11 | 12 | from cookiecutter import generate 13 | from cookiecutter.exceptions import OutputDirExistsException 14 | from cookiecutter.vcs import clone 15 | from toolz import curry 16 | 17 | 18 | def _check_name(name: str, variable_name: str) -> bool: 19 | """Check if the passed ``name`` doesn't include invalid chars.""" 20 | allowed_chars = (f'{str(set(ascii_letters.lower()))}_{digits}') 21 | try: 22 | assert all(_ in allowed_chars for _ in name) 23 | assert ' ' not in name 24 | except AssertionError: 25 | print('!!! Creation of service skipped!!!') 26 | print(f'Invalid {variable_name}: {name}!') 27 | print('Only ascii-letters and numbers are allowed!') 28 | sys.exit(1) 29 | try: 30 | assert not any(name.startswith(_) for _ in digits) 31 | except AssertionError: 32 | print('!!! Creation of service skipped!!!') 33 | print(f'Invalid {variable_name}: {name}!') 34 | print('Must not start with number!') 35 | sys.exit(1) 36 | return name 37 | 38 | 39 | def _build_arguments(args) -> Namespace: 40 | """Create required arguments for create_service.""" 41 | parser = ArgumentParser( 42 | description=( 43 | 'create new service based on fastapi using fastapi_serviceutils.' 44 | ) 45 | ) 46 | parser.add_argument( 47 | '-n', 48 | '--service_name', 49 | type=curry(_check_name, 50 | variable_name='service_name'), 51 | required=True, 52 | help=( 53 | 'the name of the service to create. ' 54 | 'ATTENTION: only ascii-letters, "_" and digits are allowed. ' 55 | 'Must not start with a digit!' 56 | ) 57 | ) 58 | parser.add_argument( 59 | '-p', 60 | '--service_port', 61 | type=str, 62 | required=True, 63 | default='50001', 64 | help='the port for the service to listen.' 65 | ) 66 | parser.add_argument( 67 | '-a', 68 | '--author', 69 | type=str, 70 | required=True, 71 | help='the name of the author of the service.' 72 | ) 73 | parser.add_argument( 74 | '-e', 75 | '--author_email', 76 | type=str, 77 | required=True, 78 | help='the email of the author of the service.' 79 | ) 80 | parser.add_argument( 81 | '-ep', 82 | '--endpoint', 83 | type=curry(_check_name, 84 | variable_name='endpoint'), 85 | required=True, 86 | help=( 87 | 'the name of the endpoint for the service to create. ' 88 | 'ATTENTION: only lower ascii-letters, "_" and digits are allowed. ' 89 | 'Must not start with a digit!' 90 | ) 91 | ) 92 | parser.add_argument('-o', '--output_dir', required=True, type=Path) 93 | return parser.parse_args(args) 94 | 95 | 96 | def _create_service_folder( 97 | repo_url: str, 98 | context: dict, 99 | output_dir: str, 100 | clone_to_dir: str = '/tmp' 101 | ) -> bool: 102 | """Clone the template and create service-folder based on this template.""" 103 | filepath = clone(repo_url, clone_to_dir=clone_to_dir, no_input=True) 104 | try: 105 | generate.generate_files( 106 | filepath, 107 | context=context, 108 | output_dir=output_dir, 109 | overwrite_if_exists=False 110 | ) 111 | except OutputDirExistsException: 112 | print('Folder already exists!') 113 | print('Skipped creation of new service!') 114 | return False 115 | return True 116 | 117 | 118 | def _create_context(params: Namespace): 119 | """Create the context required for :func:`generate.generate_files`.""" 120 | return { 121 | 'cookiecutter': { 122 | 'service_name': params.service_name, 123 | 'service_port': params.service_port, 124 | 'author': params.author, 125 | 'author_email': params.author_email, 126 | 'endpoint': params.endpoint 127 | } 128 | } 129 | 130 | 131 | def main(): 132 | """Combine required parameters, clone the template, create the service.""" 133 | repo_url = 'https://github.com/skallfass/fastapi_serviceutils_template.git' 134 | params = _build_arguments(sys.argv[1:]) 135 | create_service_result = _create_service_folder( 136 | repo_url=repo_url, 137 | context=_create_context(params), 138 | output_dir=str(params.output_dir) 139 | ) 140 | if not create_service_result: 141 | sys.exit(1) 142 | print('Service creation successful.') 143 | print(f'Service is at {params.output_dir}') 144 | 145 | 146 | if __name__ == '__main__': 147 | main() 148 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utils for services based on fastapi and fastapi-serviceutils. 2 | 3 | This include to serve an endpoint for the apidoc, the usage of 4 | external-resources and testutils. 5 | """ 6 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/docs/__init__.py: -------------------------------------------------------------------------------- 1 | """Helpers for apidocs.""" 2 | from .apidoc import mount_apidoc 3 | 4 | __all__ = ['mount_apidoc'] 5 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/docs/apidoc.py: -------------------------------------------------------------------------------- 1 | """Apidoc related functions. 2 | 3 | Contain function to mount the apidoc at route ``/apidoc`` to the service. 4 | """ 5 | import logging 6 | from typing import NoReturn 7 | 8 | from fastapi import FastAPI 9 | from starlette.staticfiles import StaticFiles 10 | 11 | 12 | def mount_apidoc(app: FastAPI) -> NoReturn: 13 | """Mount the apidoc at defined documentation_dir if it exists. 14 | 15 | Parameters: 16 | app: the app to mount the apidoc to. 17 | 18 | """ 19 | apidoc_dir = app.config.service.apidoc_dir 20 | try: 21 | app.mount('/apidoc', StaticFiles(directory=apidoc_dir), name='apidoc') 22 | logging.debug('Mounted apidoc') 23 | except RuntimeError: 24 | logging.warning(f'no doc-folder at {apidoc_dir} to serve at /apidoc') 25 | 26 | 27 | __all__ = ['mount_apidoc'] 28 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/external_resources/__init__.py: -------------------------------------------------------------------------------- 1 | """Helpers to interact with external resources like databases and services.""" 2 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/external_resources/dbs.py: -------------------------------------------------------------------------------- 1 | """Functions and classes to use databases as external_resources in service.""" 2 | from dataclasses import dataclass 3 | from typing import Dict 4 | 5 | import databases 6 | import sqlalchemy 7 | from fastapi import FastAPI 8 | from loguru._logger import Logger 9 | from pydantic import BaseModel 10 | from sqlalchemy.engine.base import Engine 11 | from sqlalchemy.sql.schema import MetaData 12 | 13 | 14 | class DatabaseDefinition(BaseModel): 15 | """Used by ``config.yml:external_resources`` to define db-dependency. 16 | 17 | Attributes: 18 | name: the name of the database. 19 | dsn: the connection string for the database. 20 | databasetype: the type of the database (currently only postgres 21 | supported). 22 | min_size: the minimum connections to open to the database. 23 | max_size: the maximum connections to open to the database. 24 | 25 | """ 26 | name: str 27 | dsn: str 28 | databasetype: str 29 | min_size: int = 5 30 | max_size: int = 20 31 | 32 | 33 | @dataclass 34 | class Database: 35 | """Class to interact with database as defined in external_resources. 36 | 37 | Attributes: 38 | dsn: the connection string for the database. 39 | logger: the logger to use inside this class. 40 | min_size: the minimum connections to open to the database. 41 | max_size: the maximum connections to open to the database. 42 | engine: the sqlalchemy engine to use. 43 | meta: the sqlalchemy metadata to use. 44 | dbase: the instance of :class:`databases.Database` to use for this 45 | database. 46 | 47 | """ 48 | dsn: str 49 | logger: Logger 50 | min_size: int 51 | max_size: int 52 | engine: Engine = None 53 | meta: MetaData = None 54 | dbase: databases.Database = None 55 | 56 | def __post_init__(self): 57 | """Set attributes ``self.dbase``, ``self.engine`` and ``self.meta``.""" 58 | self.dbase = databases.Database( 59 | self.dsn, 60 | min_size=self.min_size, 61 | max_size=self.max_size 62 | ) 63 | self.engine, self.meta = self.get_engine_metadata() 64 | 65 | def get_engine_metadata(self) -> [Engine, MetaData]: 66 | """Create the sqlalchemy-engine and -metadata for the database.""" 67 | metadata = sqlalchemy.MetaData() 68 | engine = sqlalchemy.create_engine(self.dsn) 69 | metadata.create_all(engine) 70 | return engine, metadata 71 | 72 | async def connect(self): 73 | """Open connection to the database.""" 74 | self.logger.info(f'connecting to {self.dsn}') 75 | await self.dbase.connect() 76 | 77 | async def disconnect(self): 78 | """Close connection to the database.""" 79 | self.logger.info(f'disconnecting from {self.dsn}') 80 | await self.dbase.disconnect() 81 | 82 | 83 | def add_databases_to_app( 84 | app: FastAPI, 85 | dbs: Dict[str, 86 | DatabaseDefinition] 87 | ) -> FastAPI: 88 | """Add instances of :class:`Database` as attribute of app. 89 | 90 | For each database as defined in the ``config.yml`` as external-resource, 91 | create a :class:`Database` instance with defined parameters, add this 92 | instance to the ``app.databases``-attribute and add ``startup`` and 93 | ``shutdown`` handlers to connect / disconnect to the database on 94 | app-startup / app-shutdown. 95 | 96 | Parameters: 97 | app: the app to add the database definitions to. 98 | dbs: the databases to add to the app. 99 | 100 | Returns: 101 | modified app containing the attribute app.databases and event handler 102 | for startup and shutdown for the databases. 103 | 104 | """ 105 | for _, db_definition in dbs.items(): 106 | database = Database( 107 | dsn=db_definition.dsn, 108 | logger=app.logger, 109 | min_size=db_definition.min_size, 110 | max_size=db_definition.max_size 111 | ) 112 | app.add_event_handler('startup', database.connect) 113 | app.add_event_handler('shutdown', database.disconnect) 114 | try: 115 | app.databases.update({db_definition.name: database}) 116 | except AttributeError: 117 | app.databases = {db_definition.name: database} 118 | return app 119 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/external_resources/services.py: -------------------------------------------------------------------------------- 1 | """Interact with external services.""" 2 | import logging 3 | from typing import Dict 4 | 5 | import requests 6 | from fastapi import FastAPI 7 | from fastapi import HTTPException 8 | from pydantic import BaseModel 9 | from pydantic import ValidationError 10 | 11 | 12 | class ServiceDefinition(BaseModel): 13 | """Definition for a service as defined in ``config.yml:external_resources``. 14 | 15 | Attributes: 16 | name: the name of the service. 17 | url: the url to the endpoint of the service. 18 | servicetype: the type of the service (currently only rest is 19 | supported.) 20 | 21 | """ 22 | name: str 23 | url: str 24 | servicetype: str 25 | 26 | 27 | async def _convert_response_to_model( 28 | model: BaseModel, 29 | response, 30 | info_msg: str 31 | ) -> BaseModel: 32 | """Extract request-result and convert it into an instance of ``model``. 33 | 34 | Parameters: 35 | model: the model to convert the service-result into. 36 | response: the result of the made service-call. 37 | info_msg: the message to return if something goes wrong during 38 | conversion to the model. 39 | 40 | Raises: 41 | if something goes wrong during conversion to the model a 42 | :class:`HTTPException` is raised. 43 | 44 | Returns: 45 | the service-call response converted to an instance of model. 46 | 47 | """ 48 | try: 49 | result = response.json() 50 | return model.parse_obj(result) 51 | except ValidationError as error: 52 | raise HTTPException( 53 | status_code=500, 54 | detail=( 55 | f'{info_msg} => Invalid result: {response}. Error was {error}.' 56 | ) 57 | ) 58 | 59 | 60 | async def _check_response_status(response, info_msg: str): 61 | """Check if the status of the response is valid. 62 | 63 | Parameters: 64 | response: the result of the made service-call. 65 | info_msg: the message to return if something goes wrong during 66 | service-call. 67 | 68 | Raises: 69 | if response has an invalid status-code a :class:`HTTPException` is 70 | raised. 71 | 72 | """ 73 | try: 74 | response.raise_for_status() 75 | except requests.HTTPError as error: 76 | raise HTTPException( 77 | status_code=500, 78 | detail=f'{info_msg} => Could not make request. Error was {error}.' 79 | ) 80 | 81 | 82 | async def _make_external_rest_request( 83 | url: str, 84 | method: str, 85 | params: dict, 86 | info_msg: str 87 | ): 88 | """Request external service at ``url`` using ``method`` with ``params``. 89 | 90 | Parameters: 91 | url: the url to the service-endpoint to use. 92 | method: the service-method to use. Supported are ``get`` and ``post``. 93 | params: the request-params for the service-call. 94 | info_msg: the message to return if something goes wrong during 95 | service-call. 96 | 97 | Raises: 98 | an instance of :class:`HTTPException` if something goes wrong during 99 | service-call. 100 | 101 | Returns: 102 | the result of the service-call. 103 | 104 | """ 105 | method_mapping = { 106 | 'post': requests.post, 107 | 'get': requests.get, 108 | } 109 | try: 110 | if params: 111 | return method_mapping[method](url, params=params) 112 | return method_mapping[method](url) 113 | except requests.ConnectionError as error: 114 | raise HTTPException( 115 | status_code=500, 116 | detail=f'{info_msg} => Could not connect! Error was {error}.' 117 | ) 118 | 119 | 120 | async def call_service( 121 | url: str, 122 | model: BaseModel, 123 | params: dict = None, 124 | method: str = 'post', 125 | ) -> BaseModel: 126 | """Call the rest-service at the ``url`` using ``method`` with ``params``. 127 | 128 | The result of the service-call is converted to an instance of ``model``. 129 | If any error occur this function will raise an 130 | :class:`HTTPException`. 131 | 132 | Parameters: 133 | url: the url of the service to call. 134 | model: the model to convert the service-result into. 135 | params: the params to use for the request. 136 | method: the method to use to make the service-call. 137 | 138 | Returns: 139 | the service-result as an instance of the defined ``model``. 140 | 141 | Raises: 142 | if any error occur a :class:`HTTPException` will be raised. 143 | 144 | """ 145 | info_msg = ( 146 | f'external service call (url: {url}, method: {method.upper()}, ' 147 | f'params: {params})' 148 | ) 149 | 150 | logging.debug(info_msg) 151 | 152 | # make the request for the url with params using method 153 | response = await _make_external_rest_request( 154 | url=url, 155 | method=method, 156 | params=params, 157 | info_msg=info_msg 158 | ) 159 | 160 | # check if the request worked as expected 161 | await _check_response_status(response=response, info_msg=info_msg) 162 | 163 | # convert the result of the request to an instance of model 164 | result = await _convert_response_to_model( 165 | model=model, 166 | response=response, 167 | info_msg=info_msg 168 | ) 169 | logging.debug(f'{info_msg} => returned result {result}.') 170 | return result 171 | 172 | 173 | def add_services_to_app( 174 | app: FastAPI, 175 | services: Dict[str, 176 | ServiceDefinition] 177 | ) -> FastAPI: 178 | """Add instances of :class:`ServiceDefinition` as attribute of app. 179 | 180 | For each service as defined in the ``config.yml`` as external-resource, 181 | create a :class:`ServiceDefinition` instance with defined parameters, add 182 | this instance to the ``app.services``-attribute. 183 | 184 | Parameters: 185 | app: the app to add the services as dependencies. 186 | services: the services to add to the app. 187 | 188 | Returns: 189 | modified app containing the attribute ``services`` to interact with 190 | the services inside the endpoints. 191 | 192 | """ 193 | for service_name, service_definition in services.items(): 194 | service = ServiceDefinition( 195 | url=service_definition.url, 196 | name=service_name, 197 | servicetype=service_definition.servicetype 198 | ) 199 | try: 200 | app.services.update({service_name: service}) 201 | except AttributeError: 202 | app.services = {service_name: service} 203 | return app 204 | 205 | 206 | __all__ = ['add_services_to_app', 'call_service', 'ServiceDefinition'] 207 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Utils for testing using pytest.""" 2 | -------------------------------------------------------------------------------- /fastapi_serviceutils/utils/tests/endpoints.py: -------------------------------------------------------------------------------- 1 | """Helpers to test endpoints with pytest.""" 2 | from typing import Any 3 | 4 | from fastapi import FastAPI 5 | from starlette.testclient import TestClient 6 | 7 | 8 | def json_endpoint( 9 | application: FastAPI, 10 | endpoint: str, 11 | payload: dict = None, 12 | expected: Any = None, 13 | status_code: int = 200 14 | ): 15 | """Test endpoint of app with payload.""" 16 | client = TestClient(application) 17 | if payload: 18 | response = client.post(endpoint, json=payload) 19 | else: 20 | response = client.post(endpoint) 21 | 22 | assert response.status_code == status_code 23 | 24 | if status_code == 200: 25 | result = response.json() 26 | 27 | if expected: 28 | assert result == expected 29 | 30 | return result 31 | return True 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi_serviceutils" 3 | version = "2.1.0" 4 | license = "MIT" 5 | description = "Utils for fastapi based services." 6 | authors = [ 7 | "Simon Kallfass ", 8 | ] 9 | readme = "README.md" 10 | include = ["README.md"] 11 | repository = "https://github.com/skallfass/fastapi_serviceutils" 12 | homepage = "https://fastapi-serviceutils.readthedocs.io/en/latest/" 13 | keywords = ["python", "fastapi", "webservice", "service-utils"] 14 | classifiers = [ 15 | "Operating System :: Unix", 16 | "License :: OSI Approved :: MIT License", 17 | "Programming Language :: Python :: 3.7" 18 | ] 19 | 20 | [tool.poetry.dependencies] 21 | cookiecutter = ">=1.6" 22 | databases = { version = ">=0.2", extras = ["postgresql"] } 23 | fastapi = { version = ">=0.44", extras = ["all"] } 24 | loguru = ">=0.4" 25 | psycopg2 = ">=2.8" 26 | python = ">=3.7,<4" 27 | requests = ">=2.22.0" 28 | sqlalchemy = ">=1.3" 29 | toolz = ">=0.10" 30 | 31 | [tool.poetry.dev-dependencies] 32 | autoflake = ">=1.3" 33 | coverage-badge = ">=1" 34 | flake8 = ">=3.7" 35 | ipython = ">=7.8" 36 | jedi = ">=0.14" 37 | neovim = ">=0.3.1" 38 | pudb = ">=2019.1" 39 | pygments = ">=2.4" 40 | pylint = ">=2.4.3" 41 | pytest = ">=5" 42 | pytest-asyncio = ">=0.10" 43 | pytest-cov = ">=2" 44 | pytest-xdist = ">=1.30" 45 | sphinx = ">=2" 46 | sphinx-autodoc-typehints = ">=1.6" 47 | sphinx-rtd-theme = ">=0.4.3" 48 | yapf = ">=0.27" 49 | 50 | [tool.poetry.extras] 51 | 52 | [tool.dephell.devs] 53 | from = {format = "poetry", path = "pyproject.toml"} 54 | envs = ["main", "devs"] 55 | 56 | [tool.dephell.main] 57 | from = {format = "poetry", path = "pyproject.toml"} 58 | to = {format = "setuppy", path = "setup.py"} 59 | envs = ["main"] 60 | versioning = "semver" 61 | 62 | [tool.dephell.lock] 63 | from = {format = "poetry", path = "pyproject.toml"} 64 | to = {format = "poetrylock", path = "poetry.lock"} 65 | 66 | [tool.poetry.scripts] 67 | create_service = "fastapi_serviceutils.cli.create_service:main" 68 | 69 | [build-system] 70 | requires = ["poetry>=0.12"] 71 | build-backend = "poetry.masonry.api" 72 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autoflake>=1.3 2 | cookiecutter>=1.6 3 | coverage-badge>=1 4 | databases[postgresql]>=0.2 5 | fastapi[all]>=0.44 6 | flake8>=3.7 7 | ipython>=7.8 8 | jedi>=0.14 9 | loguru>=0.4 10 | neovim>=0.3.1 11 | psycopg2>=2.8 12 | pudb>=2019.1 13 | pygments>=2.4 14 | pylint>=2.4.3 15 | pytest>=5 16 | pytest-asyncio>=0.10 17 | pytest-cov>=2 18 | pytest-xdist>=1.30 19 | requests>=2.22.0 20 | sphinx>=2 21 | sphinx-autodoc-typehints>=1.6 22 | sphinx-rtd-theme>=0.4.3 23 | sqlalchemy>=1.3 24 | toolz>=0.10 25 | yapf>=0.27 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --cov=fastapi_serviceutils 3 | --cov-report html 4 | --cov-report term-missing:skip-covered 5 | --cov-config=setup.cfg 6 | 7 | looponfailroots=fastapi_serviceutils 8 | 9 | 10 | [coverage:report] 11 | exclude_lines = 12 | pragma: no cover 13 | def __repr__ 14 | if __name__ == .__main__.: 15 | def main 16 | def script_options 17 | 18 | 19 | [flake8] 20 | max-line-length = 100 21 | 22 | 23 | [pydocstyle] 24 | convention = google 25 | match = (?!test|setup).*\.py 26 | 27 | 28 | [yapf] 29 | align_closing_bracket_with_visual_indent=False 30 | allow_multiline_dictionary_keys=False 31 | allow_multiline_lambdas=False 32 | allow_split_before_default_or_named_assigns=True 33 | allow_split_before_dict_value=True 34 | arithmetic_precedence_indication=False 35 | based_on_style = google 36 | blank_lines_around_top_level_definition=2 37 | blank_line_before_class_docstring=False 38 | blank_line_before_module_docstring=False 39 | blank_line_before_nested_class_or_def=True 40 | coalesce_brackets=False 41 | column_limit=79 42 | continuation_align_style=SPACE 43 | continuation_indent_width=4 44 | dedent_closing_brackets=True 45 | disable_ending_comma_heuristic=False 46 | each_dict_entry_on_separate_line=True 47 | i18n_comment=#\..* 48 | i18n_function_call=N_, _ 49 | indent_blank_lines=False 50 | indent_dictionary_value=False 51 | indent_width=4 52 | join_multiple_lines=True 53 | spaces_around_default_or_named_assign=False 54 | spaces_around_power_operator=False 55 | spaces_before_comment=2 56 | space_between_ending_comma_and_closing_bracket=False 57 | split_all_comma_separated_values=True 58 | split_arguments_when_comma_terminated=True 59 | split_before_arithmetic_operator=False 60 | split_before_bitwise_operator=False 61 | split_before_closing_bracket=True 62 | split_before_dict_set_generator=False 63 | split_before_dot=False 64 | split_before_expression_after_opening_paren=False 65 | split_before_first_argument=True 66 | split_before_logical_operator=False 67 | split_before_named_assigns=True 68 | split_complex_comprehension=True 69 | 70 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # DO NOT EDIT THIS FILE! 4 | # This file has been autogenerated by dephell <3 5 | # https://github.com/dephell/dephell 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | import os.path 13 | 14 | readme = '' 15 | here = os.path.abspath(os.path.dirname(__file__)) 16 | readme_path = os.path.join(here, 'README.rst') 17 | if os.path.exists(readme_path): 18 | with open(readme_path, 'rb') as stream: 19 | readme = stream.read().decode('utf8') 20 | 21 | setup( 22 | long_description=readme, 23 | name='fastapi_serviceutils', 24 | version='2.1.0', 25 | description='Utils for fastapi based services.', 26 | python_requires='<4,>=3.7', 27 | project_urls={ 28 | 'homepage': 'https://fastapi-serviceutils.readthedocs.io/en/latest/', 29 | 'repository': 'https://github.com/skallfass/fastapi_serviceutils' 30 | }, 31 | author='Simon Kallfass', 32 | author_email='skallfass@ouroboros.info', 33 | license='MIT', 34 | keywords='python fastapi webservice service-utils', 35 | classifiers=[ 36 | 'Operating System :: Unix', 'License :: OSI Approved :: MIT License', 37 | 'Programming Language :: Python :: 3.7' 38 | ], 39 | entry_points={ 40 | 'console_scripts': [ 41 | 'create_service = fastapi_serviceutils.cli.create_service:main' 42 | ] 43 | }, 44 | packages=[ 45 | 'fastapi_serviceutils', 'fastapi_serviceutils.app', 46 | 'fastapi_serviceutils.app.endpoints', 47 | 'fastapi_serviceutils.app.endpoints.default', 48 | 'fastapi_serviceutils.app.handlers', 49 | 'fastapi_serviceutils.app.middlewares', 'fastapi_serviceutils.cli', 50 | 'fastapi_serviceutils.utils', 'fastapi_serviceutils.utils.docs', 51 | 'fastapi_serviceutils.utils.external_resources', 52 | 'fastapi_serviceutils.utils.tests' 53 | ], 54 | package_data={}, 55 | install_requires=[ 56 | 'cookiecutter>=1.6', 'databases[postgresql]>=0.2', 'fastapi[all]>=0.44', 57 | 'loguru>=0.4', 'psycopg2>=2.8', 'requests>=2.22.0', 'sqlalchemy>=1.3', 58 | 'toolz>=0.10' 59 | ], 60 | extras_require={ 61 | 'dev': [ 62 | 'autoflake>=1.3', 'coverage-badge>=1', 'flake8>=3.7', 63 | 'ipython>=7.8', 'jedi>=0.14', 'neovim>=0.3.1', 'pudb>=2019.1', 64 | 'pygments>=2.4', 'pylint>=2.4.3', 'pytest>=5', 65 | 'pytest-asyncio>=0.10', 'pytest-cov>=2', 'pytest-xdist>=1.30', 66 | 'sphinx>=2', 'sphinx-autodoc-typehints>=1.6', 67 | 'sphinx-rtd-theme>=0.4.3', 'yapf>=0.27' 68 | ] 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skallfass/fastapi_serviceutils/8033450fc556396c735fb7c783ebe36e28a60ac0/tests/__init__.py -------------------------------------------------------------------------------- /tests/base_service_config_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from pydantic import ValidationError 5 | 6 | from fastapi_serviceutils.app import collect_config_definition 7 | from fastapi_serviceutils.app import Config 8 | 9 | 10 | @pytest.mark.parametrize( 11 | 'config_path', 12 | [ 13 | 'tests/configs/config.yml', 14 | 'tests/configs/config2.yml', 15 | 'tests/configs/config3.yml', 16 | ] 17 | ) 18 | def test_collect_config_definition(config_path): 19 | config_path = Path(config_path) 20 | config = collect_config_definition(config_path=config_path) 21 | assert isinstance(config, Config) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | 'config_path', 26 | [ 27 | 'tests/invalid_configs/invalid_config.yml', 28 | 'tests/invalid_configs/invalid_config2.yml', 29 | 'tests/invalid_configs/invalid_config3.yml', 30 | 'tests/invalid_configs/invalid_config4.yml', 31 | 'tests/invalid_configs/invalid_config5.yml', 32 | 'tests/invalid_configs/invalid_config6.yml', 33 | 'tests/invalid_configs/invalid_config7.yml', 34 | 'tests/invalid_configs/invalid_config8.yml', 35 | 'tests/invalid_configs/invalid_config9.yml', 36 | 'tests/invalid_configs/invalid_config10.yml', 37 | 'tests/invalid_configs/invalid_config11.yml', 38 | 'tests/invalid_configs/invalid_config12.yml', 39 | 'tests/invalid_configs/invalid_config13.yml', 40 | 'tests/invalid_configs/invalid_config14.yml', 41 | 'tests/invalid_configs/invalid_config15.yml', 42 | 'tests/invalid_configs/invalid_config16.yml', 43 | 'tests/invalid_configs/invalid_config17.yml', 44 | 'tests/invalid_configs/invalid_config18.yml', 45 | 'tests/invalid_configs/invalid_config19.yml', 46 | 'tests/invalid_configs/invalid_config20.yml', 47 | ] 48 | ) 49 | def test_collect_config_definition_invalid(config_path): 50 | config_path = Path(config_path) 51 | with pytest.raises(ValidationError): 52 | collect_config_definition(config_path=config_path) 53 | -------------------------------------------------------------------------------- /tests/configs/config.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: 15 | testservice: 16 | url: 'https://httpbin.org/post' 17 | servicetype: 'rest' 18 | databases: 19 | testdb: 20 | dsn: 'postgresql://postgres:1234@localhost:50005/monitordb' 21 | databasetype: 'postgres' 22 | min_size: 5 23 | max_size: 20 24 | other: null 25 | logger: 26 | path: './log/EXAMPLESERVICE' 27 | filename: 'service_{mode}.log' 28 | level: 'debug' 29 | rotation: '1 days' 30 | retention: '1 months' 31 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 32 | available_environment_variables: 33 | env_vars: 34 | - EXAMPLESERVICE__SERVICE__MODE 35 | - EXAMPLESERVICE__SERVICE__PORT 36 | - EXAMPLESERVICE__LOGGER__LEVEL 37 | - EXAMPLESERVICE__LOGGER__PATH 38 | - EXAMPLESERVICE__LOGGER__FILENAME 39 | - EXAMPLESERVICE__LOGGER__ROTATION 40 | - EXAMPLESERVICE__LOGGER__RETENTION 41 | - EXAMPLESERVICE__LOGGER__FORMAT 42 | external_resources_env_vars: [] 43 | rules_env_vars: [] 44 | -------------------------------------------------------------------------------- /tests/configs/config2.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | external_resources: 13 | services: null 14 | databases: null 15 | other: null 16 | logger: 17 | path: './log/EXAMPLESERVICE' 18 | filename: 'service_{mode}.log' 19 | level: 'debug' 20 | rotation: '1 days' 21 | retention: '1 months' 22 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 23 | available_environment_variables: 24 | env_vars: 25 | - EXAMPLESERVICE_SERVICE__MODE 26 | - EXAMPLESERVICE_SERVICE__PORT 27 | - EXAMPLESERVICE_LOGGER__LEVEL 28 | - EXAMPLESERVICE_LOGGER__PATH 29 | - EXAMPLESERVICE_LOGGER__FILENAME 30 | - EXAMPLESERVICE_LOGGER__ROTATION 31 | - EXAMPLESERVICE_LOGGER__RETENTION 32 | - EXAMPLESERVICE_LOGGER__FORMAT 33 | external_resources_env_vars: [] 34 | rules_env_vars: [] 35 | -------------------------------------------------------------------------------- /tests/configs/config3.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'test' 4 | port: 5002 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: null 16 | other: 17 | something: 18 | containing: 19 | - 1 20 | - {} 21 | - [] 22 | logger: 23 | path: './log/EXAMPLESERVICE' 24 | filename: 'service_{mode}.log' 25 | level: 'debug' 26 | rotation: '1 days' 27 | retention: '1 months' 28 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 29 | available_environment_variables: 30 | env_vars: [] 31 | external_resources_env_vars: [] 32 | rules_env_vars: [] 33 | -------------------------------------------------------------------------------- /tests/create_service_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from argparse import Namespace 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from fastapi_serviceutils.cli.create_service import _build_arguments 8 | from fastapi_serviceutils.cli.create_service import _check_name 9 | from fastapi_serviceutils.cli.create_service import _create_context 10 | from fastapi_serviceutils.cli.create_service import _create_service_folder 11 | 12 | 13 | @pytest.mark.parametrize( 14 | 'name', 15 | ['fastapi', 16 | 'fastapi_serviceutils', 17 | 'pathlib2'] 18 | ) 19 | def test_check_name(name: str): 20 | assert _check_name(name=name, variable_name='test') 21 | 22 | 23 | @pytest.mark.parametrize( 24 | 'name', 25 | ['1fastapi', 26 | 'fastapi serviceutils', 27 | 'pathlib-2'] 28 | ) 29 | def test_check_name_invalid(name: str): 30 | with pytest.raises(SystemExit) as pytest_wrapped_e: 31 | _check_name(name=name, variable_name='test') 32 | assert pytest_wrapped_e.type == SystemExit 33 | assert pytest_wrapped_e.value.code == 1 34 | 35 | 36 | @pytest.mark.parametrize('output_dir', ['/tmp']) 37 | @pytest.mark.parametrize('endpoint', ['example', 'example1', 'example_1']) 38 | @pytest.mark.parametrize('author_email', ['bla@blub.info']) 39 | @pytest.mark.parametrize('author', ['bla']) 40 | @pytest.mark.parametrize('service_port', ['50000', '50001']) 41 | @pytest.mark.parametrize( 42 | 'service_name', 43 | ['fastapi', 44 | 'fastapi_serviceutils', 45 | 'pathlib2'] 46 | ) 47 | def test_build_arguments( 48 | service_name: str, 49 | service_port: str, 50 | author: str, 51 | author_email: str, 52 | endpoint: str, 53 | output_dir: str 54 | ): 55 | params = _build_arguments( 56 | [ 57 | '--service_name', 58 | service_name, 59 | '--service_port', 60 | service_port, 61 | '--author', 62 | author, 63 | '--author_email', 64 | author_email, 65 | '--endpoint', 66 | endpoint, 67 | '--output_dir', 68 | output_dir 69 | ] 70 | ) 71 | assert params.service_name == service_name 72 | assert params.service_port == service_port 73 | assert params.author == author 74 | assert params.author_email == author_email 75 | assert params.endpoint == endpoint 76 | assert params.output_dir == Path(output_dir) 77 | 78 | 79 | def test_create_context(): 80 | params = Namespace( 81 | service_name='exampleservice', 82 | service_port='50000', 83 | author='john smith', 84 | author_email='jsmith@something.info', 85 | endpoint='example', 86 | output_dir=Path('/tmp') 87 | ) 88 | result = _create_context(params=params) 89 | assert result['cookiecutter'] 90 | 91 | 92 | def test_create_service_folder(tmpdir): 93 | repo_url = 'git+ssh://git@github.com/skallfass/fastapi_serviceutils_template.git' 94 | params = Namespace( 95 | service_name='exampleservice', 96 | service_port='50000', 97 | author='john smith', 98 | author_email='jsmith@something.info', 99 | endpoint='example', 100 | output_dir=Path('/tmp') 101 | ) 102 | context = _create_context(params=params) 103 | clone_to_dir = str(tmpdir) 104 | assert _create_service_folder( 105 | repo_url=repo_url, 106 | context=context, 107 | output_dir=str(tmpdir), 108 | clone_to_dir=clone_to_dir 109 | ) 110 | assert (tmpdir / 'exampleservice').exists() 111 | assert (tmpdir / 'fastapi_serviceutils_template').exists() 112 | assert not _create_service_folder( 113 | repo_url=repo_url, 114 | context=context, 115 | output_dir=str(tmpdir), 116 | clone_to_dir=clone_to_dir 117 | ) 118 | -------------------------------------------------------------------------------- /tests/default_endpoints_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from fastapi_serviceutils import make_app 4 | from fastapi_serviceutils.app import Config 5 | from fastapi_serviceutils.utils.tests.endpoints import json_endpoint 6 | 7 | app = make_app( 8 | config_path=Path('tests/configs/config.yml'), 9 | version='0.1.0', 10 | endpoints=[], 11 | enable_middlewares=[], 12 | additional_middlewares=[], 13 | ) 14 | 15 | 16 | def test_endpoint_alive(): 17 | """Test if endpoint "/api/alive/" works as expected.""" 18 | json_endpoint( 19 | application=app, 20 | endpoint='/api/alive/', 21 | expected={'alive': True} 22 | ) 23 | 24 | 25 | def test_endpoint_config(): 26 | """Test if endpoint "/api/config/" works as expected.""" 27 | result = json_endpoint(application=app, endpoint='/api/config/') 28 | print(result) 29 | assert isinstance(Config.parse_obj(result), Config) 30 | -------------------------------------------------------------------------------- /tests/endpoints_test.py: -------------------------------------------------------------------------------- 1 | from fastapi_serviceutils.app import Endpoint 2 | from fastapi_serviceutils.app.endpoints import set_version_endpoints 3 | from fastapi_serviceutils.app.endpoints.default.alive import ENDPOINT 4 | 5 | 6 | def test_set_version_endpoints(): 7 | endpoints = set_version_endpoints( 8 | endpoints=[ENDPOINT], 9 | version='v2', 10 | prefix_template='/api/{version}{route}' 11 | ) 12 | endpoint = endpoints[0] 13 | assert isinstance(endpoints, list) 14 | assert isinstance(endpoint, Endpoint) 15 | assert endpoint.version == 'v2' 16 | assert endpoint.route == f'/api/v2{ENDPOINT.route}' 17 | assert endpoint.tags == ['v2'] 18 | latest = set_version_endpoints( 19 | endpoints=endpoints, 20 | version='latest', 21 | prefix_template='{route}' 22 | ) 23 | endpoint = latest[0] 24 | assert isinstance(latest, list) 25 | assert isinstance(endpoint, Endpoint) 26 | assert endpoint.version == 'latest' 27 | assert endpoint.route == f'/api/latest{ENDPOINT.route}' 28 | assert endpoint.tags == ['latest'] 29 | -------------------------------------------------------------------------------- /tests/external_resources_db_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from sqlalchemy import Boolean 4 | from sqlalchemy import Column 5 | from sqlalchemy import insert 6 | from sqlalchemy import Integer 7 | from sqlalchemy import String 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from starlette.testclient import TestClient 10 | 11 | from fastapi_serviceutils import make_app 12 | 13 | Base = declarative_base() 14 | app = make_app( 15 | config_path=Path('tests/configs/config.yml'), 16 | version='0.1.0', 17 | endpoints=[], 18 | enable_middlewares=[], 19 | additional_middlewares=[], 20 | ) 21 | 22 | 23 | class User(Base): 24 | __tablename__ = 'users' 25 | 26 | id = Column(Integer, primary_key=True, index=True) 27 | email = Column(String, unique=True, index=True) 28 | hashed_password = Column(String) 29 | is_active = Column(Boolean, default=True) 30 | 31 | 32 | @app.post('/test') 33 | async def example_endpoint(): 34 | database = app.databases['testdb'].dbase 35 | async with database.transaction(force_rollback=True): 36 | Base.metadata.create_all( 37 | app.databases['testdb'].engine, 38 | tables=[User.__table__] 39 | ) 40 | query = insert(User).values( 41 | email='test', 42 | hashed_password='bla', 43 | is_active=True 44 | ) 45 | await database.execute(query) 46 | return await database.fetch_all(User.__table__.select()) 47 | 48 | 49 | def test_create_schema(): 50 | expected = {'email': 'test', 'hashed_password': 'bla', 'is_active': True} 51 | with TestClient(app) as client: 52 | response = client.post('/test') 53 | result = response.json()[0] 54 | result.pop('id') 55 | assert result == expected 56 | -------------------------------------------------------------------------------- /tests/external_resources_external_service_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any 3 | 4 | from pydantic import BaseModel 5 | from pydantic import Schema 6 | from starlette.testclient import TestClient 7 | 8 | from fastapi_serviceutils import make_app 9 | from fastapi_serviceutils.utils.external_resources.services import call_service 10 | 11 | app = make_app( 12 | config_path=Path('tests/configs/config.yml'), 13 | version='0.1.0', 14 | endpoints=[], 15 | enable_middlewares=[], 16 | additional_middlewares=[], 17 | ) 18 | 19 | 20 | class ExampleModel(BaseModel): 21 | args: dict 22 | data: str 23 | files: dict 24 | form: dict 25 | headers: dict 26 | origin: str 27 | url: str 28 | json_: Any = Schema(None, alias='json') 29 | 30 | 31 | @app.post('/test') 32 | async def serviceendpoint(): 33 | url = app.services['testservice'].url 34 | response = await call_service(url=url, params=None, model=ExampleModel) 35 | return response 36 | 37 | 38 | def test_call_rest_service(): 39 | """Test if endpoint "/api/alive/" works as expected.""" 40 | with TestClient(app) as client: 41 | response = client.post('/test') 42 | assert ExampleModel.parse_obj(response.json()) 43 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config.yml: -------------------------------------------------------------------------------- 1 | service: 2 | mode: 'devl' 3 | port: 50001 4 | description: 'Example tasks' 5 | apidoc_dir: 'docs/_build' 6 | readme: 'README.md' 7 | allowed_hosts: 8 | - '*' 9 | use_default_endpoints: 10 | - alive 11 | - config 12 | external_resources: 13 | services: null 14 | databases: 15 | testdb: 16 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 17 | databasetype: 'postgres' 18 | other: null 19 | logger: 20 | path: './log/EXAMPLESERVICE' 21 | filename: 'service_{mode}.log' 22 | level: 'debug' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config10.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | filename: 'service_{mode}.log' 22 | level: 'debug' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config11.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | level: 'debug' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config12.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config13.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | level: 'debug' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config14.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | level: 'debug' 24 | rotation: '1 days' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config15.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | level: 'debug' 24 | rotation: '1 days' 25 | retention: '1 months' 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config16.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | available_environment_variables: 21 | env_vars: 22 | - EXAMPLESERVICE_SERVICE__MODE 23 | - EXAMPLESERVICE_SERVICE__PORT 24 | - EXAMPLESERVICE_LOGGER__LEVEL 25 | - EXAMPLESERVICE_LOGGER__PATH 26 | - EXAMPLESERVICE_LOGGER__FILENAME 27 | - EXAMPLESERVICE_LOGGER__ROTATION 28 | - EXAMPLESERVICE_LOGGER__RETENTION 29 | - EXAMPLESERVICE_LOGGER__FORMAT 30 | external_resources_env_vars: [] 31 | rules_env_vars: [] 32 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config17.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | level: 'debug' 24 | rotation: '1 days' 25 | retention: '1 months' 26 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 27 | available_environment_variables: 28 | external_resources_env_vars: [] 29 | rules_env_vars: [] 30 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config18.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | level: 'debug' 24 | rotation: '1 days' 25 | retention: '1 months' 26 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 27 | available_environment_variables: 28 | env_vars: 29 | - EXAMPLESERVICE_SERVICE__MODE 30 | - EXAMPLESERVICE_SERVICE__PORT 31 | - EXAMPLESERVICE_LOGGER__LEVEL 32 | - EXAMPLESERVICE_LOGGER__PATH 33 | - EXAMPLESERVICE_LOGGER__FILENAME 34 | - EXAMPLESERVICE_LOGGER__ROTATION 35 | - EXAMPLESERVICE_LOGGER__RETENTION 36 | - EXAMPLESERVICE_LOGGER__FORMAT 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config19.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | level: 'debug' 24 | rotation: '1 days' 25 | retention: '1 months' 26 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 27 | available_environment_variables: 28 | env_vars: 29 | - EXAMPLESERVICE_SERVICE__MODE 30 | - EXAMPLESERVICE_SERVICE__PORT 31 | - EXAMPLESERVICE_LOGGER__LEVEL 32 | - EXAMPLESERVICE_LOGGER__PATH 33 | - EXAMPLESERVICE_LOGGER__FILENAME 34 | - EXAMPLESERVICE_LOGGER__ROTATION 35 | - EXAMPLESERVICE_LOGGER__RETENTION 36 | - EXAMPLESERVICE_LOGGER__FORMAT 37 | external_resources_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config2.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | port: 50001 4 | description: 'Example tasks' 5 | apidoc_dir: 'docs/_build' 6 | readme: 'README.md' 7 | allowed_hosts: 8 | - '*' 9 | use_default_endpoints: 10 | - alive 11 | - config 12 | external_resources: 13 | services: null 14 | databases: 15 | testdb: 16 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 17 | databasetype: 'postgres' 18 | other: null 19 | logger: 20 | path: './log/EXAMPLESERVICE' 21 | filename: 'service_{mode}.log' 22 | level: 'debug' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config20.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | external_resources: 14 | services: null 15 | databases: 16 | testdb: 17 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 18 | databasetype: 'postgres' 19 | other: null 20 | logger: 21 | path: './log/EXAMPLESERVICE' 22 | filename: 'service_{mode}.log' 23 | level: 'debug' 24 | rotation: '1 days' 25 | retention: '1 months' 26 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 27 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config3.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | description: 'Example tasks' 5 | apidoc_dir: 'docs/_build' 6 | readme: 'README.md' 7 | allowed_hosts: 8 | - '*' 9 | use_default_endpoints: 10 | - alive 11 | - config 12 | external_resources: 13 | services: null 14 | databases: 15 | testdb: 16 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 17 | databasetype: 'postgres' 18 | other: null 19 | logger: 20 | path: './log/EXAMPLESERVICE' 21 | filename: 'service_{mode}.log' 22 | level: 'debug' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config4.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | apidoc_dir: 'docs/_build' 6 | readme: 'README.md' 7 | allowed_hosts: 8 | - '*' 9 | use_default_endpoints: 10 | - alive 11 | - config 12 | external_resources: 13 | services: null 14 | databases: 15 | testdb: 16 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 17 | databasetype: 'postgres' 18 | other: null 19 | logger: 20 | path: './log/EXAMPLESERVICE' 21 | filename: 'service_{mode}.log' 22 | level: 'debug' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config5.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | allowed_hosts: 8 | - '*' 9 | use_default_endpoints: 10 | - alive 11 | - config 12 | external_resources: 13 | services: null 14 | databases: 15 | testdb: 16 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 17 | databasetype: 'postgres' 18 | other: null 19 | logger: 20 | path: './log/EXAMPLESERVICE' 21 | filename: 'service_{mode}.log' 22 | level: 'debug' 23 | rotation: '1 days' 24 | retention: '1 months' 25 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 26 | available_environment_variables: 27 | env_vars: 28 | - EXAMPLESERVICE_SERVICE__MODE 29 | - EXAMPLESERVICE_SERVICE__PORT 30 | - EXAMPLESERVICE_LOGGER__LEVEL 31 | - EXAMPLESERVICE_LOGGER__PATH 32 | - EXAMPLESERVICE_LOGGER__FILENAME 33 | - EXAMPLESERVICE_LOGGER__ROTATION 34 | - EXAMPLESERVICE_LOGGER__RETENTION 35 | - EXAMPLESERVICE_LOGGER__FORMAT 36 | external_resources_env_vars: [] 37 | rules_env_vars: [] 38 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config6.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | use_default_endpoints: 9 | - alive 10 | - config 11 | external_resources: 12 | services: null 13 | databases: 14 | testdb: 15 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 16 | databasetype: 'postgres' 17 | other: null 18 | logger: 19 | path: './log/EXAMPLESERVICE' 20 | filename: 'service_{mode}.log' 21 | level: 'debug' 22 | rotation: '1 days' 23 | retention: '1 months' 24 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 25 | available_environment_variables: 26 | env_vars: 27 | - EXAMPLESERVICE_SERVICE__MODE 28 | - EXAMPLESERVICE_SERVICE__PORT 29 | - EXAMPLESERVICE_LOGGER__LEVEL 30 | - EXAMPLESERVICE_LOGGER__PATH 31 | - EXAMPLESERVICE_LOGGER__FILENAME 32 | - EXAMPLESERVICE_LOGGER__ROTATION 33 | - EXAMPLESERVICE_LOGGER__RETENTION 34 | - EXAMPLESERVICE_LOGGER__FORMAT 35 | external_resources_env_vars: [] 36 | rules_env_vars: [] 37 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config7.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | external_resources: 11 | services: null 12 | databases: 13 | testdb: 14 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 15 | databasetype: 'postgres' 16 | other: null 17 | logger: 18 | path: './log/EXAMPLESERVICE' 19 | filename: 'service_{mode}.log' 20 | level: 'debug' 21 | rotation: '1 days' 22 | retention: '1 months' 23 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 24 | available_environment_variables: 25 | env_vars: 26 | - EXAMPLESERVICE_SERVICE__MODE 27 | - EXAMPLESERVICE_SERVICE__PORT 28 | - EXAMPLESERVICE_LOGGER__LEVEL 29 | - EXAMPLESERVICE_LOGGER__PATH 30 | - EXAMPLESERVICE_LOGGER__FILENAME 31 | - EXAMPLESERVICE_LOGGER__ROTATION 32 | - EXAMPLESERVICE_LOGGER__RETENTION 33 | - EXAMPLESERVICE_LOGGER__FORMAT 34 | external_resources_env_vars: [] 35 | rules_env_vars: [] 36 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config8.yml: -------------------------------------------------------------------------------- 1 | external_resources: 2 | services: null 3 | databases: 4 | testdb: 5 | dsn: 'postgresql://postgres:1234@localhost:5434/monitordb' 6 | databasetype: 'postgres' 7 | other: null 8 | logger: 9 | path: './log/EXAMPLESERVICE' 10 | filename: 'service_{mode}.log' 11 | level: 'debug' 12 | rotation: '1 days' 13 | retention: '1 months' 14 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 15 | available_environment_variables: 16 | env_vars: 17 | - EXAMPLESERVICE_SERVICE__MODE 18 | - EXAMPLESERVICE_SERVICE__PORT 19 | - EXAMPLESERVICE_LOGGER__LEVEL 20 | - EXAMPLESERVICE_LOGGER__PATH 21 | - EXAMPLESERVICE_LOGGER__FILENAME 22 | - EXAMPLESERVICE_LOGGER__ROTATION 23 | - EXAMPLESERVICE_LOGGER__RETENTION 24 | - EXAMPLESERVICE_LOGGER__FORMAT 25 | external_resources_env_vars: [] 26 | rules_env_vars: [] 27 | -------------------------------------------------------------------------------- /tests/invalid_configs/invalid_config9.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: 'exampleservice' 3 | mode: 'devl' 4 | port: 50001 5 | description: 'Example tasks' 6 | apidoc_dir: 'docs/_build' 7 | readme: 'README.md' 8 | allowed_hosts: 9 | - '*' 10 | use_default_endpoints: 11 | - alive 12 | - config 13 | logger: 14 | path: './log/EXAMPLESERVICE' 15 | filename: 'service_{mode}.log' 16 | level: 'debug' 17 | rotation: '1 days' 18 | retention: '1 months' 19 | format: "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} [{extra[request_id]}] - {message}" 20 | available_environment_variables: 21 | env_vars: 22 | - EXAMPLESERVICE_SERVICE__MODE 23 | - EXAMPLESERVICE_SERVICE__PORT 24 | - EXAMPLESERVICE_LOGGER__LEVEL 25 | - EXAMPLESERVICE_LOGGER__PATH 26 | - EXAMPLESERVICE_LOGGER__FILENAME 27 | - EXAMPLESERVICE_LOGGER__ROTATION 28 | - EXAMPLESERVICE_LOGGER__RETENTION 29 | - EXAMPLESERVICE_LOGGER__FORMAT 30 | external_resources_env_vars: [] 31 | rules_env_vars: [] 32 | --------------------------------------------------------------------------------