mirror of
https://github.com/FutureOfMedTech-FITM-hack/backend.git
synced 2024-11-21 22:16:33 +03:00
Initial commit
This commit is contained in:
commit
d95e557ad9
144
.dockerignore
Normal file
144
.dockerignore
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
### Python template
|
||||||
|
|
||||||
|
deploy/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.git/
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
31
.editorconfig
Normal file
31
.editorconfig
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
tab_width = 4
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 88
|
||||||
|
ij_visual_guides = 88
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{js,py,html}]
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[.flake8]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
ij_python_from_import_parentheses_force_if_multiline = true
|
113
.flake8
Normal file
113
.flake8
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
[flake8]
|
||||||
|
max-complexity = 6
|
||||||
|
inline-quotes = double
|
||||||
|
max-line-length = 88
|
||||||
|
extend-ignore = E203
|
||||||
|
docstring_style=sphinx
|
||||||
|
|
||||||
|
ignore =
|
||||||
|
; Found `f` string
|
||||||
|
WPS305,
|
||||||
|
; Missing docstring in public module
|
||||||
|
D100,
|
||||||
|
; Missing docstring in magic method
|
||||||
|
D105,
|
||||||
|
; Missing docstring in __init__
|
||||||
|
D107,
|
||||||
|
; Found `__init__.py` module with logic
|
||||||
|
WPS412,
|
||||||
|
; Found class without a base class
|
||||||
|
WPS306,
|
||||||
|
; Missing docstring in public nested class
|
||||||
|
D106,
|
||||||
|
; First line should be in imperative mood
|
||||||
|
D401,
|
||||||
|
; Found wrong variable name
|
||||||
|
WPS110,
|
||||||
|
; Found `__init__.py` module with logic
|
||||||
|
WPS326,
|
||||||
|
; Found string constant over-use
|
||||||
|
WPS226,
|
||||||
|
; Found upper-case constant in a class
|
||||||
|
WPS115,
|
||||||
|
; Found nested function
|
||||||
|
WPS602,
|
||||||
|
; Found method without arguments
|
||||||
|
WPS605,
|
||||||
|
; Found overused expression
|
||||||
|
WPS204,
|
||||||
|
; Found too many module members
|
||||||
|
WPS202,
|
||||||
|
; Found too high module cognitive complexity
|
||||||
|
WPS232,
|
||||||
|
; line break before binary operator
|
||||||
|
W503,
|
||||||
|
; Found module with too many imports
|
||||||
|
WPS201,
|
||||||
|
; Inline strong start-string without end-string.
|
||||||
|
RST210,
|
||||||
|
; Found nested class
|
||||||
|
WPS431,
|
||||||
|
; Found wrong module name
|
||||||
|
WPS100,
|
||||||
|
; Found too many methods
|
||||||
|
WPS214,
|
||||||
|
; Found too long ``try`` body
|
||||||
|
WPS229,
|
||||||
|
; Found unpythonic getter or setter
|
||||||
|
WPS615,
|
||||||
|
; Found a line that starts with a dot
|
||||||
|
WPS348,
|
||||||
|
; Found complex default value (for dependency injection)
|
||||||
|
WPS404,
|
||||||
|
; not perform function calls in argument defaults (for dependency injection)
|
||||||
|
B008,
|
||||||
|
; Model should define verbose_name in its Meta inner class
|
||||||
|
DJ10,
|
||||||
|
; Model should define verbose_name_plural in its Meta inner class
|
||||||
|
DJ11,
|
||||||
|
; Found mutable module constant.
|
||||||
|
WPS407,
|
||||||
|
; Found too many empty lines in `def`
|
||||||
|
WPS473,
|
||||||
|
|
||||||
|
per-file-ignores =
|
||||||
|
; all tests
|
||||||
|
test_*.py,tests.py,tests_*.py,*/tests/*,conftest.py:
|
||||||
|
; Use of assert detected
|
||||||
|
S101,
|
||||||
|
; Found outer scope names shadowing
|
||||||
|
WPS442,
|
||||||
|
; Found too many local variables
|
||||||
|
WPS210,
|
||||||
|
; Found magic number
|
||||||
|
WPS432,
|
||||||
|
; Missing parameter(s) in Docstring
|
||||||
|
DAR101,
|
||||||
|
; Found too many arguments
|
||||||
|
WPS211,
|
||||||
|
|
||||||
|
; all init files
|
||||||
|
__init__.py:
|
||||||
|
; ignore not used imports
|
||||||
|
F401,
|
||||||
|
; ignore import with wildcard
|
||||||
|
F403,
|
||||||
|
; Found wrong metadata variable
|
||||||
|
WPS410,
|
||||||
|
|
||||||
|
exclude =
|
||||||
|
./.cache,
|
||||||
|
./.git,
|
||||||
|
./.idea,
|
||||||
|
./.mypy_cache,
|
||||||
|
./.pytest_cache,
|
||||||
|
./.venv,
|
||||||
|
./venv,
|
||||||
|
./env,
|
||||||
|
./cached_venv,
|
||||||
|
./docs,
|
||||||
|
./deploy,
|
||||||
|
./var,
|
||||||
|
./.vscode,
|
||||||
|
*migrations*,
|
142
.gitignore
vendored
Normal file
142
.gitignore
vendored
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
### Python template
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
62
.pre-commit-config.yaml
Normal file
62
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.4.0
|
||||||
|
hooks:
|
||||||
|
- id: check-ast
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: check-toml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
|
||||||
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
|
rev: v2.1.0
|
||||||
|
hooks:
|
||||||
|
- id: add-trailing-comma
|
||||||
|
|
||||||
|
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||||
|
rev: v2.1.0
|
||||||
|
hooks:
|
||||||
|
- id: pretty-format-yaml
|
||||||
|
args:
|
||||||
|
- --autofix
|
||||||
|
- --preserve-quotes
|
||||||
|
- --indent=2
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
name: Format with Black
|
||||||
|
entry: poetry run black
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
|
||||||
|
- id: autoflake
|
||||||
|
name: autoflake
|
||||||
|
entry: poetry run autoflake
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
args: [--in-place, --remove-all-unused-imports, --remove-duplicate-keys]
|
||||||
|
|
||||||
|
- id: isort
|
||||||
|
name: isort
|
||||||
|
entry: poetry run isort
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
|
||||||
|
- id: flake8
|
||||||
|
name: Check with Flake8
|
||||||
|
entry: poetry run flake8
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
types: [python]
|
||||||
|
args: [--count, .]
|
||||||
|
|
||||||
|
- id: mypy
|
||||||
|
name: Validate types with MyPy
|
||||||
|
entry: poetry run mypy
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
pass_filenames: false
|
||||||
|
args:
|
||||||
|
- "med_backend"
|
132
README.md
Normal file
132
README.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
# med_backend
|
||||||
|
|
||||||
|
This project was generated using fastapi_template.
|
||||||
|
|
||||||
|
## Poetry
|
||||||
|
|
||||||
|
This project uses poetry. It's a modern dependency management
|
||||||
|
tool.
|
||||||
|
|
||||||
|
To run the project use this set of commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry install
|
||||||
|
poetry run python -m med_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the server on the configured host.
|
||||||
|
|
||||||
|
You can find swagger documentation at `/api/docs`.
|
||||||
|
|
||||||
|
You can read more about poetry here: https://python-poetry.org/
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
You can start the project with docker using this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f deploy/docker-compose.yml --project-directory . up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to develop in docker with autoreload add `-f deploy/docker-compose.dev.yml` to your docker command.
|
||||||
|
Like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml --project-directory . up
|
||||||
|
```
|
||||||
|
|
||||||
|
This command exposes the web application on port 8000, mounts current directory and enables autoreload.
|
||||||
|
|
||||||
|
But you have to rebuild image every time you modify `poetry.lock` or `pyproject.toml` with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f deploy/docker-compose.yml --project-directory . build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ tree "med_backend"
|
||||||
|
med_backend
|
||||||
|
├── conftest.py # Fixtures for all tests.
|
||||||
|
├── db # module contains db configurations
|
||||||
|
│ ├── dao # Data Access Objects. Contains different classes to inteact with database.
|
||||||
|
│ └── models # Package contains different models for ORMs.
|
||||||
|
├── __main__.py # Startup script. Starts uvicorn.
|
||||||
|
├── services # Package for different external services such as rabbit or redis etc.
|
||||||
|
├── settings.py # Main configuration settings for project.
|
||||||
|
├── static # Static content.
|
||||||
|
├── tests # Tests for project.
|
||||||
|
└── web # Package contains web server. Handlers, startup config.
|
||||||
|
├── api # Package with all handlers.
|
||||||
|
│ └── router.py # Main router.
|
||||||
|
├── application.py # FastAPI application configuration.
|
||||||
|
└── lifetime.py # Contains actions to perform on startup and shutdown.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This application can be configured with environment variables.
|
||||||
|
|
||||||
|
You can create `.env` file in the root directory and place all
|
||||||
|
environment variables here.
|
||||||
|
|
||||||
|
All environment variabels should start with "MED_BACKEND_" prefix.
|
||||||
|
|
||||||
|
For example if you see in your "med_backend/settings.py" a variable named like
|
||||||
|
`random_parameter`, you should provide the "MED_BACKEND_RANDOM_PARAMETER"
|
||||||
|
variable to configure the value. This behaviour can be changed by overriding `env_prefix` property
|
||||||
|
in `med_backend.settings.Settings.Config`.
|
||||||
|
|
||||||
|
An exmaple of .env file:
|
||||||
|
```bash
|
||||||
|
MED_BACKEND_RELOAD="True"
|
||||||
|
MED_BACKEND_PORT="8000"
|
||||||
|
MED_BACKEND_ENVIRONMENT="dev"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can read more about BaseSettings class here: https://pydantic-docs.helpmanual.io/usage/settings/
|
||||||
|
|
||||||
|
## Pre-commit
|
||||||
|
|
||||||
|
To install pre-commit simply run inside the shell:
|
||||||
|
```bash
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
pre-commit is very useful to check your code before publishing it.
|
||||||
|
It's configured using .pre-commit-config.yaml file.
|
||||||
|
|
||||||
|
By default it runs:
|
||||||
|
* black (formats your code);
|
||||||
|
* mypy (validates types);
|
||||||
|
* isort (sorts imports in all files);
|
||||||
|
* flake8 (spots possibe bugs);
|
||||||
|
* yesqa (removes useless `# noqa` comments).
|
||||||
|
|
||||||
|
|
||||||
|
You can read more about pre-commit here: https://pre-commit.com/
|
||||||
|
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
If you want to run it in docker, simply run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f deploy/docker-compose.yml --project-directory . run --rm api pytest -vv .
|
||||||
|
docker-compose -f deploy/docker-compose.yml --project-directory . down
|
||||||
|
```
|
||||||
|
|
||||||
|
For running tests on your local machine.
|
||||||
|
1. you need to start a database.
|
||||||
|
|
||||||
|
I prefer doing it with docker:
|
||||||
|
```
|
||||||
|
docker run -p "5432:5432" -e "POSTGRES_PASSWORD=med_backend" -e "POSTGRES_USER=med_backend" -e "POSTGRES_DB=med_backend" postgres:13.8-bullseye
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
2. Run the pytest.
|
||||||
|
```bash
|
||||||
|
pytest -vv .
|
||||||
|
```
|
27
deploy/Dockerfile
Normal file
27
deploy/Dockerfile
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
FROM python:3.9.6-slim-buster
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
RUN pip install poetry==1.2.2
|
||||||
|
|
||||||
|
# Configuring poetry
|
||||||
|
RUN poetry config virtualenvs.create false
|
||||||
|
|
||||||
|
# Copying requirements of a project
|
||||||
|
COPY pyproject.toml poetry.lock /app/src/
|
||||||
|
WORKDIR /app/src
|
||||||
|
|
||||||
|
# Installing requirements
|
||||||
|
RUN poetry install
|
||||||
|
# Removing gcc
|
||||||
|
RUN apt-get purge -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copying actuall application
|
||||||
|
COPY . /app/src/
|
||||||
|
RUN poetry install
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/python", "-m", "med_backend"]
|
13
deploy/docker-compose.dev.yml
Normal file
13
deploy/docker-compose.dev.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
ports:
|
||||||
|
# Exposes application port.
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
# Adds current directory as volume.
|
||||||
|
- .:/app/src/
|
||||||
|
environment:
|
||||||
|
# Enables autoreload.
|
||||||
|
MED_BACKEND_RELOAD: "True"
|
57
deploy/docker-compose.yml
Normal file
57
deploy/docker-compose.yml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./deploy/Dockerfile
|
||||||
|
image: med_backend:${MED_BACKEND_VERSION:-latest}
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
MED_BACKEND_HOST: 0.0.0.0
|
||||||
|
MED_BACKEND_DB_HOST: med_backend-db
|
||||||
|
MED_BACKEND_DB_PORT: 5432
|
||||||
|
MED_BACKEND_DB_USER: med_backend
|
||||||
|
MED_BACKEND_DB_PASS: med_backend
|
||||||
|
MED_BACKEND_DB_BASE: med_backend
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:13.8-bullseye
|
||||||
|
hostname: med_backend-db
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: "med_backend"
|
||||||
|
POSTGRES_USER: "med_backend"
|
||||||
|
POSTGRES_DB: "med_backend"
|
||||||
|
volumes:
|
||||||
|
- med_backend-db-data:/var/lib/postgresql/data
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U med_backend
|
||||||
|
interval: 2s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 40
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: bitnami/redis:6.2.5
|
||||||
|
hostname: "med_backend-redis"
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
ALLOW_EMPTY_PASSWORD: "yes"
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping
|
||||||
|
interval: 1s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 50
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
med_backend-db-data:
|
||||||
|
name: med_backend-db-data
|
1
med_backend/__init__.py
Normal file
1
med_backend/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""med_backend package."""
|
20
med_backend/__main__.py
Normal file
20
med_backend/__main__.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from med_backend.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Entrypoint of the application."""
|
||||||
|
uvicorn.run(
|
||||||
|
"med_backend.web.application:get_app",
|
||||||
|
workers=settings.workers_count,
|
||||||
|
host=settings.host,
|
||||||
|
port=settings.port,
|
||||||
|
reload=settings.reload,
|
||||||
|
log_level=settings.log_level.value.lower(),
|
||||||
|
factory=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
129
med_backend/conftest.py
Normal file
129
med_backend/conftest.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
from typing import Any, AsyncGenerator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fakeredis import FakeServer
|
||||||
|
from fakeredis.aioredis import FakeConnection
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from redis.asyncio import ConnectionPool
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from med_backend.db.dependencies import get_db_session
|
||||||
|
from med_backend.db.utils import create_database, drop_database
|
||||||
|
from med_backend.services.redis.dependency import get_redis_pool
|
||||||
|
from med_backend.settings import settings
|
||||||
|
from med_backend.web.application import get_app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def anyio_backend() -> str:
|
||||||
|
"""
|
||||||
|
Backend for anyio pytest plugin.
|
||||||
|
|
||||||
|
:return: backend name.
|
||||||
|
"""
|
||||||
|
return "asyncio"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
async def _engine() -> AsyncGenerator[AsyncEngine, None]:
|
||||||
|
"""
|
||||||
|
Create engine and databases.
|
||||||
|
|
||||||
|
:yield: new engine.
|
||||||
|
"""
|
||||||
|
from med_backend.db.meta import meta # noqa: WPS433
|
||||||
|
from med_backend.db.models import load_all_models # noqa: WPS433
|
||||||
|
|
||||||
|
load_all_models()
|
||||||
|
|
||||||
|
await create_database()
|
||||||
|
|
||||||
|
engine = create_async_engine(str(settings.db_url))
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(meta.create_all)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield engine
|
||||||
|
finally:
|
||||||
|
await engine.dispose()
|
||||||
|
await drop_database()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def dbsession(
|
||||||
|
_engine: AsyncEngine,
|
||||||
|
) -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
"""
|
||||||
|
Get session to database.
|
||||||
|
|
||||||
|
Fixture that returns a SQLAlchemy session with a SAVEPOINT, and the rollback to it
|
||||||
|
after the test completes.
|
||||||
|
|
||||||
|
:param _engine: current engine.
|
||||||
|
:yields: async session.
|
||||||
|
"""
|
||||||
|
connection = await _engine.connect()
|
||||||
|
trans = await connection.begin()
|
||||||
|
|
||||||
|
session_maker = sessionmaker(
|
||||||
|
connection,
|
||||||
|
expire_on_commit=False,
|
||||||
|
class_=AsyncSession,
|
||||||
|
)
|
||||||
|
session = session_maker()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
await trans.rollback()
|
||||||
|
await connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def fake_redis_pool() -> AsyncGenerator[ConnectionPool, None]:
|
||||||
|
"""
|
||||||
|
Get instance of a fake redis.
|
||||||
|
|
||||||
|
:yield: FakeRedis instance.
|
||||||
|
"""
|
||||||
|
server = FakeServer()
|
||||||
|
server.connected = True
|
||||||
|
pool = ConnectionPool(connection_class=FakeConnection, server=server)
|
||||||
|
|
||||||
|
yield pool
|
||||||
|
|
||||||
|
await pool.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fastapi_app(
|
||||||
|
dbsession: AsyncSession,
|
||||||
|
fake_redis_pool: ConnectionPool,
|
||||||
|
) -> FastAPI:
|
||||||
|
"""
|
||||||
|
Fixture for creating FastAPI app.
|
||||||
|
|
||||||
|
:return: fastapi app with mocked dependencies.
|
||||||
|
"""
|
||||||
|
application = get_app()
|
||||||
|
application.dependency_overrides[get_db_session] = lambda: dbsession
|
||||||
|
application.dependency_overrides[get_redis_pool] = lambda: fake_redis_pool
|
||||||
|
return application # noqa: WPS331
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(
|
||||||
|
fastapi_app: FastAPI,
|
||||||
|
anyio_backend: Any,
|
||||||
|
) -> AsyncGenerator[AsyncClient, None]:
|
||||||
|
"""
|
||||||
|
Fixture that creates client for requesting server.
|
||||||
|
|
||||||
|
:param fastapi_app: the application.
|
||||||
|
:yield: client for the app.
|
||||||
|
"""
|
||||||
|
async with AsyncClient(app=fastapi_app, base_url="http://test") as ac:
|
||||||
|
yield ac
|
20
med_backend/db/base.py
Normal file
20
med_backend/db/base.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from typing import Any, Tuple
|
||||||
|
|
||||||
|
from sqlalchemy import Table
|
||||||
|
from sqlalchemy.orm import as_declarative
|
||||||
|
|
||||||
|
from med_backend.db.meta import meta
|
||||||
|
|
||||||
|
|
||||||
|
@as_declarative(metadata=meta)
|
||||||
|
class Base:
|
||||||
|
"""
|
||||||
|
Base for all models.
|
||||||
|
|
||||||
|
It has some type definitions to
|
||||||
|
enhance autocompletion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str
|
||||||
|
__table__: Table
|
||||||
|
__table_args__: Tuple[Any, ...]
|
1
med_backend/db/dao/__init__.py
Normal file
1
med_backend/db/dao/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""DAO classes."""
|
53
med_backend/db/dao/dummy_dao.py
Normal file
53
med_backend/db/dao/dummy_dao.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from med_backend.db.dependencies import get_db_session
|
||||||
|
from med_backend.db.models.dummy_model import DummyModel
|
||||||
|
|
||||||
|
|
||||||
|
class DummyDAO:
|
||||||
|
"""Class for accessing dummy table."""
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession = Depends(get_db_session)):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def create_dummy_model(self, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Add single dummy to session.
|
||||||
|
|
||||||
|
:param name: name of a dummy.
|
||||||
|
"""
|
||||||
|
self.session.add(DummyModel(name=name))
|
||||||
|
|
||||||
|
async def get_all_dummies(self, limit: int, offset: int) -> List[DummyModel]:
|
||||||
|
"""
|
||||||
|
Get all dummy models with limit/offset pagination.
|
||||||
|
|
||||||
|
:param limit: limit of dummies.
|
||||||
|
:param offset: offset of dummies.
|
||||||
|
:return: stream of dummies.
|
||||||
|
"""
|
||||||
|
raw_dummies = await self.session.execute(
|
||||||
|
select(DummyModel).limit(limit).offset(offset),
|
||||||
|
)
|
||||||
|
|
||||||
|
return raw_dummies.scalars().fetchall()
|
||||||
|
|
||||||
|
async def filter(
|
||||||
|
self,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
) -> List[DummyModel]:
|
||||||
|
"""
|
||||||
|
Get specific dummy model.
|
||||||
|
|
||||||
|
:param name: name of dummy instance.
|
||||||
|
:return: dummy models.
|
||||||
|
"""
|
||||||
|
query = select(DummyModel)
|
||||||
|
if name:
|
||||||
|
query = query.where(DummyModel.name == name)
|
||||||
|
rows = await self.session.execute(query)
|
||||||
|
return rows.scalars().fetchall()
|
20
med_backend/db/dependencies.py
Normal file
20
med_backend/db/dependencies.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
"""
|
||||||
|
Create and get database session.
|
||||||
|
|
||||||
|
:param request: current request.
|
||||||
|
:yield: database session.
|
||||||
|
"""
|
||||||
|
session: AsyncSession = request.app.state.db_session_factory()
|
||||||
|
|
||||||
|
try: # noqa: WPS501
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
await session.commit()
|
||||||
|
await session.close()
|
3
med_backend/db/meta.py
Normal file
3
med_backend/db/meta.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
meta = sa.MetaData()
|
14
med_backend/db/models/__init__.py
Normal file
14
med_backend/db/models/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
"""med_backend models."""
|
||||||
|
import pkgutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load_all_models() -> None:
|
||||||
|
"""Load all models from this folder."""
|
||||||
|
package_dir = Path(__file__).resolve().parent
|
||||||
|
modules = pkgutil.walk_packages(
|
||||||
|
path=[str(package_dir)],
|
||||||
|
prefix="med_backend.db.models.",
|
||||||
|
)
|
||||||
|
for module in modules:
|
||||||
|
__import__(module.name) # noqa: WPS421
|
13
med_backend/db/models/dummy_model.py
Normal file
13
med_backend/db/models/dummy_model.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from sqlalchemy.sql.schema import Column
|
||||||
|
from sqlalchemy.sql.sqltypes import Integer, String
|
||||||
|
|
||||||
|
from med_backend.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class DummyModel(Base):
|
||||||
|
"""Model for demo purpose."""
|
||||||
|
|
||||||
|
__tablename__ = "dummy_model"
|
||||||
|
|
||||||
|
id = Column(Integer(), primary_key=True, autoincrement=True)
|
||||||
|
name = Column(String(length=200)) # noqa: WPS432
|
44
med_backend/db/utils.py
Normal file
44
med_backend/db/utils.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.engine import make_url
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
|
||||||
|
from med_backend.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
async def create_database() -> None:
|
||||||
|
"""Create a databse."""
|
||||||
|
db_url = make_url(str(settings.db_url.with_path("/postgres")))
|
||||||
|
engine = create_async_engine(db_url, isolation_level="AUTOCOMMIT")
|
||||||
|
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
database_existance = await conn.execute(
|
||||||
|
text(
|
||||||
|
f"SELECT 1 FROM pg_database WHERE datname='{settings.db_base}'", # noqa: E501, S608
|
||||||
|
),
|
||||||
|
)
|
||||||
|
database_exists = database_existance.scalar() == 1
|
||||||
|
|
||||||
|
if database_exists:
|
||||||
|
await drop_database()
|
||||||
|
|
||||||
|
async with engine.connect() as conn: # noqa: WPS440
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
f'CREATE DATABASE "{settings.db_base}" ENCODING "utf8" TEMPLATE template1', # noqa: E501
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def drop_database() -> None:
|
||||||
|
"""Drop current database."""
|
||||||
|
db_url = make_url(str(settings.db_url.with_path("/postgres")))
|
||||||
|
engine = create_async_engine(db_url, isolation_level="AUTOCOMMIT")
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
disc_users = (
|
||||||
|
"SELECT pg_terminate_backend(pg_stat_activity.pid) " # noqa: S608
|
||||||
|
"FROM pg_stat_activity "
|
||||||
|
f"WHERE pg_stat_activity.datname = '{settings.db_base}' "
|
||||||
|
"AND pid <> pg_backend_pid();"
|
||||||
|
)
|
||||||
|
await conn.execute(text(disc_users))
|
||||||
|
await conn.execute(text(f'DROP DATABASE "{settings.db_base}"'))
|
1
med_backend/services/__init__.py
Normal file
1
med_backend/services/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Services for med_backend."""
|
1
med_backend/services/redis/__init__.py
Normal file
1
med_backend/services/redis/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Redis service."""
|
26
med_backend/services/redis/dependency.py
Normal file
26
med_backend/services/redis/dependency.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
|
||||||
|
async def get_redis_pool(
|
||||||
|
request: Request,
|
||||||
|
) -> AsyncGenerator[Redis, None]: # pragma: no cover
|
||||||
|
"""
|
||||||
|
Returns connection pool.
|
||||||
|
|
||||||
|
You can use it like this:
|
||||||
|
|
||||||
|
>>> from redis.asyncio import ConnectionPool, Redis
|
||||||
|
>>>
|
||||||
|
>>> async def handler(redis_pool: ConnectionPool = Depends(get_redis_pool)):
|
||||||
|
>>> async with Redis(connection_pool=redis_pool) as redis:
|
||||||
|
>>> await redis.get('key')
|
||||||
|
|
||||||
|
I use pools so you don't acquire connection till the end of the handler.
|
||||||
|
|
||||||
|
:param request: current request.
|
||||||
|
:returns: redis connection pool.
|
||||||
|
"""
|
||||||
|
return request.app.state.redis_pool
|
24
med_backend/services/redis/lifetime.py
Normal file
24
med_backend/services/redis/lifetime.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from redis.asyncio import ConnectionPool
|
||||||
|
|
||||||
|
from med_backend.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
def init_redis(app: FastAPI) -> None: # pragma: no cover
|
||||||
|
"""
|
||||||
|
Creates connection pool for redis.
|
||||||
|
|
||||||
|
:param app: current fastapi application.
|
||||||
|
"""
|
||||||
|
app.state.redis_pool = ConnectionPool.from_url(
|
||||||
|
str(settings.redis_url),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def shutdown_redis(app: FastAPI) -> None: # pragma: no cover
|
||||||
|
"""
|
||||||
|
Closes redis connection pool.
|
||||||
|
|
||||||
|
:param app: current FastAPI app.
|
||||||
|
"""
|
||||||
|
await app.state.redis_pool.disconnect()
|
99
med_backend/settings.py
Normal file
99
med_backend/settings.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import enum
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import gettempdir
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseSettings
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
|
TEMP_DIR = Path(gettempdir())
|
||||||
|
|
||||||
|
|
||||||
|
class LogLevel(str, enum.Enum): # noqa: WPS600
|
||||||
|
"""Possible log levels."""
|
||||||
|
|
||||||
|
NOTSET = "NOTSET"
|
||||||
|
DEBUG = "DEBUG"
|
||||||
|
INFO = "INFO"
|
||||||
|
WARNING = "WARNING"
|
||||||
|
ERROR = "ERROR"
|
||||||
|
FATAL = "FATAL"
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""
|
||||||
|
Application settings.
|
||||||
|
|
||||||
|
These parameters can be configured
|
||||||
|
with environment variables.
|
||||||
|
"""
|
||||||
|
|
||||||
|
host: str = "127.0.0.1"
|
||||||
|
port: int = 8000
|
||||||
|
# quantity of workers for uvicorn
|
||||||
|
workers_count: int = 1
|
||||||
|
# Enable uvicorn reloading
|
||||||
|
reload: bool = False
|
||||||
|
|
||||||
|
# Current environment
|
||||||
|
environment: str = "dev"
|
||||||
|
|
||||||
|
log_level: LogLevel = LogLevel.INFO
|
||||||
|
|
||||||
|
# Variables for the database
|
||||||
|
db_host: str = "localhost"
|
||||||
|
db_port: int = 5432
|
||||||
|
db_user: str = "med_backend"
|
||||||
|
db_pass: str = "med_backend"
|
||||||
|
db_base: str = "med_backend"
|
||||||
|
db_echo: bool = False
|
||||||
|
|
||||||
|
# Variables for Redis
|
||||||
|
redis_host: str = "med_backend-redis"
|
||||||
|
redis_port: int = 6379
|
||||||
|
redis_user: Optional[str] = None
|
||||||
|
redis_pass: Optional[str] = None
|
||||||
|
redis_base: Optional[int] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_url(self) -> URL:
|
||||||
|
"""
|
||||||
|
Assemble database URL from settings.
|
||||||
|
|
||||||
|
:return: database URL.
|
||||||
|
"""
|
||||||
|
return URL.build(
|
||||||
|
scheme="postgresql+asyncpg",
|
||||||
|
host=self.db_host,
|
||||||
|
port=self.db_port,
|
||||||
|
user=self.db_user,
|
||||||
|
password=self.db_pass,
|
||||||
|
path=f"/{self.db_base}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def redis_url(self) -> URL:
|
||||||
|
"""
|
||||||
|
Assemble REDIS URL from settings.
|
||||||
|
|
||||||
|
:return: redis URL.
|
||||||
|
"""
|
||||||
|
path = ""
|
||||||
|
if self.redis_base is not None:
|
||||||
|
path = f"/{self.redis_base}"
|
||||||
|
return URL.build(
|
||||||
|
scheme="redis",
|
||||||
|
host=self.redis_host,
|
||||||
|
port=self.redis_port,
|
||||||
|
user=self.redis_user,
|
||||||
|
password=self.redis_pass,
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
env_prefix = "MED_BACKEND_"
|
||||||
|
env_file_encoding = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
107
med_backend/static/docs/redoc.standalone.js
Normal file
107
med_backend/static/docs/redoc.standalone.js
Normal file
File diff suppressed because one or more lines are too long
3
med_backend/static/docs/swagger-ui-bundle.js
Normal file
3
med_backend/static/docs/swagger-ui-bundle.js
Normal file
File diff suppressed because one or more lines are too long
4
med_backend/static/docs/swagger-ui.css
Normal file
4
med_backend/static/docs/swagger-ui.css
Normal file
File diff suppressed because one or more lines are too long
1
med_backend/tests/__init__.py
Normal file
1
med_backend/tests/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for med_backend."""
|
49
med_backend/tests/test_dummy.py
Normal file
49
med_backend/tests/test_dummy.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from starlette import status
|
||||||
|
|
||||||
|
from med_backend.db.dao.dummy_dao import DummyDAO
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_creation(
|
||||||
|
fastapi_app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
dbsession: AsyncSession,
|
||||||
|
) -> None:
|
||||||
|
"""Tests dummy instance creation."""
|
||||||
|
url = fastapi_app.url_path_for("create_dummy_model")
|
||||||
|
test_name = uuid.uuid4().hex
|
||||||
|
response = await client.put(
|
||||||
|
url,
|
||||||
|
json={
|
||||||
|
"name": test_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
dao = DummyDAO(dbsession)
|
||||||
|
instances = await dao.filter(name=test_name)
|
||||||
|
assert instances[0].name == test_name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_getting(
|
||||||
|
fastapi_app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
dbsession: AsyncSession,
|
||||||
|
) -> None:
|
||||||
|
"""Tests dummy instance retrieval."""
|
||||||
|
dao = DummyDAO(dbsession)
|
||||||
|
test_name = uuid.uuid4().hex
|
||||||
|
await dao.create_dummy_model(name=test_name)
|
||||||
|
url = fastapi_app.url_path_for("get_dummy_models")
|
||||||
|
response = await client.get(url)
|
||||||
|
dummies = response.json()
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert len(dummies) == 1
|
||||||
|
assert dummies[0]["name"] == test_name
|
26
med_backend/tests/test_echo.py
Normal file
26
med_backend/tests/test_echo.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from starlette import status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_echo(fastapi_app: FastAPI, client: AsyncClient) -> None:
|
||||||
|
"""
|
||||||
|
Tests that echo route works.
|
||||||
|
|
||||||
|
:param fastapi_app: current application.
|
||||||
|
:param client: clien for the app.
|
||||||
|
"""
|
||||||
|
url = fastapi_app.url_path_for("send_echo_message")
|
||||||
|
message = uuid.uuid4().hex
|
||||||
|
response = await client.post(
|
||||||
|
url,
|
||||||
|
json={
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.json()["message"] == message
|
17
med_backend/tests/test_med_backend.py
Normal file
17
med_backend/tests/test_med_backend.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from starlette import status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_health(client: AsyncClient, fastapi_app: FastAPI) -> None:
|
||||||
|
"""
|
||||||
|
Checks the health endpoint.
|
||||||
|
|
||||||
|
:param client: client for the app.
|
||||||
|
:param fastapi_app: current FastAPI application.
|
||||||
|
"""
|
||||||
|
url = fastapi_app.url_path_for("health_check")
|
||||||
|
response = await client.get(url)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
63
med_backend/tests/test_redis.py
Normal file
63
med_backend/tests/test_redis.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from redis.asyncio import ConnectionPool, Redis
|
||||||
|
from starlette import status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_setting_value(
|
||||||
|
fastapi_app: FastAPI,
|
||||||
|
fake_redis_pool: ConnectionPool,
|
||||||
|
client: AsyncClient,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Tests that you can set value in redis.
|
||||||
|
|
||||||
|
:param fastapi_app: current application fixture.
|
||||||
|
:param fake_redis_pool: fake redis pool.
|
||||||
|
:param client: client fixture.
|
||||||
|
"""
|
||||||
|
url = fastapi_app.url_path_for("set_redis_value")
|
||||||
|
|
||||||
|
test_key = uuid.uuid4().hex
|
||||||
|
test_val = uuid.uuid4().hex
|
||||||
|
response = await client.put(
|
||||||
|
url,
|
||||||
|
json={
|
||||||
|
"key": test_key,
|
||||||
|
"value": test_val,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
async with Redis(connection_pool=fake_redis_pool) as redis:
|
||||||
|
actual_value = await redis.get(test_key)
|
||||||
|
assert actual_value.decode() == test_val
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_getting_value(
|
||||||
|
fastapi_app: FastAPI,
|
||||||
|
fake_redis_pool: ConnectionPool,
|
||||||
|
client: AsyncClient,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Tests that you can get value from redis by key.
|
||||||
|
|
||||||
|
:param fastapi_app: current application fixture.
|
||||||
|
:param fake_redis_pool: fake redis pool.
|
||||||
|
:param client: client fixture.
|
||||||
|
"""
|
||||||
|
test_key = uuid.uuid4().hex
|
||||||
|
test_val = uuid.uuid4().hex
|
||||||
|
async with Redis(connection_pool=fake_redis_pool) as redis:
|
||||||
|
await redis.set(test_key, test_val)
|
||||||
|
url = fastapi_app.url_path_for("get_redis_value")
|
||||||
|
response = await client.get(url, params={"key": test_key})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.json()["key"] == test_key
|
||||||
|
assert response.json()["value"] == test_val
|
1
med_backend/web/__init__.py
Normal file
1
med_backend/web/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""WEB API for med_backend."""
|
1
med_backend/web/api/__init__.py
Normal file
1
med_backend/web/api/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""med_backend API package."""
|
4
med_backend/web/api/dummy/__init__.py
Normal file
4
med_backend/web/api/dummy/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""Dummy model API."""
|
||||||
|
from med_backend.web.api.dummy.views import router
|
||||||
|
|
||||||
|
__all__ = ["router"]
|
21
med_backend/web/api/dummy/schema.py
Normal file
21
med_backend/web/api/dummy/schema.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class DummyModelDTO(BaseModel):
|
||||||
|
"""
|
||||||
|
DTO for dummy models.
|
||||||
|
|
||||||
|
It returned when accessing dummy models from the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class DummyModelInputDTO(BaseModel):
|
||||||
|
"""DTO for creating new dummy model."""
|
||||||
|
|
||||||
|
name: str
|
41
med_backend/web/api/dummy/views.py
Normal file
41
med_backend/web/api/dummy/views.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.param_functions import Depends
|
||||||
|
|
||||||
|
from med_backend.db.dao.dummy_dao import DummyDAO
|
||||||
|
from med_backend.db.models.dummy_model import DummyModel
|
||||||
|
from med_backend.web.api.dummy.schema import DummyModelDTO, DummyModelInputDTO
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[DummyModelDTO])
|
||||||
|
async def get_dummy_models(
|
||||||
|
limit: int = 10,
|
||||||
|
offset: int = 0,
|
||||||
|
dummy_dao: DummyDAO = Depends(),
|
||||||
|
) -> List[DummyModel]:
|
||||||
|
"""
|
||||||
|
Retrieve all dummy objects from the database.
|
||||||
|
|
||||||
|
:param limit: limit of dummy objects, defaults to 10.
|
||||||
|
:param offset: offset of dummy objects, defaults to 0.
|
||||||
|
:param dummy_dao: DAO for dummy models.
|
||||||
|
:return: list of dummy obbjects from database.
|
||||||
|
"""
|
||||||
|
return await dummy_dao.get_all_dummies(limit=limit, offset=offset)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/")
|
||||||
|
async def create_dummy_model(
|
||||||
|
new_dummy_object: DummyModelInputDTO,
|
||||||
|
dummy_dao: DummyDAO = Depends(),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Creates dummy model in the database.
|
||||||
|
|
||||||
|
:param new_dummy_object: new dummy model item.
|
||||||
|
:param dummy_dao: DAO for dummy models.
|
||||||
|
"""
|
||||||
|
await dummy_dao.create_dummy_model(**new_dummy_object.dict())
|
4
med_backend/web/api/echo/__init__.py
Normal file
4
med_backend/web/api/echo/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""Echo API."""
|
||||||
|
from med_backend.web.api.echo.views import router
|
||||||
|
|
||||||
|
__all__ = ["router"]
|
7
med_backend/web/api/echo/schema.py
Normal file
7
med_backend/web/api/echo/schema.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Message(BaseModel):
|
||||||
|
"""Simple message model."""
|
||||||
|
|
||||||
|
message: str
|
18
med_backend/web/api/echo/views.py
Normal file
18
med_backend/web/api/echo/views.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from med_backend.web.api.echo.schema import Message
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Message)
|
||||||
|
async def send_echo_message(
|
||||||
|
incoming_message: Message,
|
||||||
|
) -> Message:
|
||||||
|
"""
|
||||||
|
Sends echo back to user.
|
||||||
|
|
||||||
|
:param incoming_message: incoming message.
|
||||||
|
:returns: message same as the incoming.
|
||||||
|
"""
|
||||||
|
return incoming_message
|
4
med_backend/web/api/monitoring/__init__.py
Normal file
4
med_backend/web/api/monitoring/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""API for checking project status."""
|
||||||
|
from med_backend.web.api.monitoring.views import router
|
||||||
|
|
||||||
|
__all__ = ["router"]
|
12
med_backend/web/api/monitoring/views.py
Normal file
12
med_backend/web/api/monitoring/views.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
def health_check() -> None:
|
||||||
|
"""
|
||||||
|
Checks the health of a project.
|
||||||
|
|
||||||
|
It returns 200 if the project is healthy.
|
||||||
|
"""
|
4
med_backend/web/api/redis/__init__.py
Normal file
4
med_backend/web/api/redis/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""Redis API."""
|
||||||
|
from med_backend.web.api.redis.views import router
|
||||||
|
|
||||||
|
__all__ = ["router"]
|
10
med_backend/web/api/redis/schema.py
Normal file
10
med_backend/web/api/redis/schema.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class RedisValueDTO(BaseModel):
|
||||||
|
"""DTO for redis values."""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
value: Optional[str] # noqa: WPS110
|
44
med_backend/web/api/redis/views.py
Normal file
44
med_backend/web/api/redis/views.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.param_functions import Depends
|
||||||
|
from redis.asyncio import ConnectionPool, Redis
|
||||||
|
|
||||||
|
from med_backend.services.redis.dependency import get_redis_pool
|
||||||
|
from med_backend.web.api.redis.schema import RedisValueDTO
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=RedisValueDTO)
|
||||||
|
async def get_redis_value(
|
||||||
|
key: str,
|
||||||
|
redis_pool: ConnectionPool = Depends(get_redis_pool),
|
||||||
|
) -> RedisValueDTO:
|
||||||
|
"""
|
||||||
|
Get value from redis.
|
||||||
|
|
||||||
|
:param key: redis key, to get data from.
|
||||||
|
:param redis_pool: redis connection pool.
|
||||||
|
:returns: information from redis.
|
||||||
|
"""
|
||||||
|
async with Redis(connection_pool=redis_pool) as redis:
|
||||||
|
redis_value = await redis.get(key)
|
||||||
|
return RedisValueDTO(
|
||||||
|
key=key,
|
||||||
|
value=redis_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/")
|
||||||
|
async def set_redis_value(
|
||||||
|
redis_value: RedisValueDTO,
|
||||||
|
redis_pool: ConnectionPool = Depends(get_redis_pool),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set value in redis.
|
||||||
|
|
||||||
|
:param redis_value: new value data.
|
||||||
|
:param redis_pool: redis connection pool.
|
||||||
|
"""
|
||||||
|
if redis_value.value is not None:
|
||||||
|
async with Redis(connection_pool=redis_pool) as redis:
|
||||||
|
await redis.set(name=redis_value.key, value=redis_value.value)
|
9
med_backend/web/api/router.py
Normal file
9
med_backend/web/api/router.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
|
||||||
|
from med_backend.web.api import dummy, echo, monitoring, redis
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
api_router.include_router(monitoring.router)
|
||||||
|
api_router.include_router(echo.router, prefix="/echo", tags=["echo"])
|
||||||
|
api_router.include_router(dummy.router, prefix="/dummy", tags=["dummy"])
|
||||||
|
api_router.include_router(redis.router, prefix="/redis", tags=["redis"])
|
35
med_backend/web/application.py
Normal file
35
med_backend/web/application.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from importlib import metadata
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import UJSONResponse
|
||||||
|
|
||||||
|
from med_backend.web.api.router import api_router
|
||||||
|
from med_backend.web.lifetime import register_shutdown_event, register_startup_event
|
||||||
|
|
||||||
|
|
||||||
|
def get_app() -> FastAPI:
|
||||||
|
"""
|
||||||
|
Get FastAPI application.
|
||||||
|
|
||||||
|
This is the main constructor of an application.
|
||||||
|
|
||||||
|
:return: application.
|
||||||
|
"""
|
||||||
|
app = FastAPI(
|
||||||
|
title="med_backend",
|
||||||
|
description="",
|
||||||
|
version=metadata.version("med_backend"),
|
||||||
|
docs_url="/api/docs",
|
||||||
|
redoc_url="/api/redoc",
|
||||||
|
openapi_url="/api/openapi.json",
|
||||||
|
default_response_class=UJSONResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Adds startup and shutdown events.
|
||||||
|
register_startup_event(app)
|
||||||
|
register_shutdown_event(app)
|
||||||
|
|
||||||
|
# Main router for the API.
|
||||||
|
app.include_router(router=api_router, prefix="/api")
|
||||||
|
|
||||||
|
return app
|
90
med_backend/web/lifetime.py
Normal file
90
med_backend/web/lifetime.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
from asyncio import current_task
|
||||||
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from sqlalchemy.ext.asyncio import (
|
||||||
|
AsyncSession,
|
||||||
|
async_scoped_session,
|
||||||
|
create_async_engine,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from med_backend.db.meta import meta
|
||||||
|
from med_backend.db.models import load_all_models
|
||||||
|
from med_backend.services.redis.lifetime import init_redis, shutdown_redis
|
||||||
|
from med_backend.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_db(app: FastAPI) -> None: # pragma: no cover
|
||||||
|
"""
|
||||||
|
Creates connection to the database.
|
||||||
|
|
||||||
|
This function creates SQLAlchemy engine instance,
|
||||||
|
session_factory for creating sessions
|
||||||
|
and stores them in the application's state property.
|
||||||
|
|
||||||
|
:param app: fastAPI application.
|
||||||
|
"""
|
||||||
|
engine = create_async_engine(str(settings.db_url), echo=settings.db_echo)
|
||||||
|
session_factory = async_scoped_session(
|
||||||
|
sessionmaker(
|
||||||
|
engine,
|
||||||
|
expire_on_commit=False,
|
||||||
|
class_=AsyncSession,
|
||||||
|
),
|
||||||
|
scopefunc=current_task,
|
||||||
|
)
|
||||||
|
app.state.db_engine = engine
|
||||||
|
app.state.db_session_factory = session_factory
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_tables() -> None: # pragma: no cover
|
||||||
|
"""Populates tables in the database."""
|
||||||
|
load_all_models()
|
||||||
|
engine = create_async_engine(str(settings.db_url))
|
||||||
|
async with engine.begin() as connection:
|
||||||
|
await connection.run_sync(meta.create_all)
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def register_startup_event(
|
||||||
|
app: FastAPI,
|
||||||
|
) -> Callable[[], Awaitable[None]]: # pragma: no cover
|
||||||
|
"""
|
||||||
|
Actions to run on application startup.
|
||||||
|
|
||||||
|
This function uses fastAPI app to store data
|
||||||
|
inthe state, such as db_engine.
|
||||||
|
|
||||||
|
:param app: the fastAPI application.
|
||||||
|
:return: function that actually performs actions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def _startup() -> None: # noqa: WPS430
|
||||||
|
_setup_db(app)
|
||||||
|
await _create_tables()
|
||||||
|
init_redis(app)
|
||||||
|
pass # noqa: WPS420
|
||||||
|
|
||||||
|
return _startup
|
||||||
|
|
||||||
|
|
||||||
|
def register_shutdown_event(
|
||||||
|
app: FastAPI,
|
||||||
|
) -> Callable[[], Awaitable[None]]: # pragma: no cover
|
||||||
|
"""
|
||||||
|
Actions to run on application's shutdown.
|
||||||
|
|
||||||
|
:param app: fastAPI application.
|
||||||
|
:return: function that actually performs actions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def _shutdown() -> None: # noqa: WPS430
|
||||||
|
await app.state.db_engine.dispose()
|
||||||
|
|
||||||
|
await shutdown_redis(app)
|
||||||
|
pass # noqa: WPS420
|
||||||
|
|
||||||
|
return _shutdown
|
2210
poetry.lock
generated
Normal file
2210
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
81
pyproject.toml
Normal file
81
pyproject.toml
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "med_backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [
|
||||||
|
|
||||||
|
]
|
||||||
|
maintainers = [
|
||||||
|
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9"
|
||||||
|
fastapi = "^0.85.0"
|
||||||
|
uvicorn = { version = "^0.18.3", extras = ["standard"] }
|
||||||
|
pydantic = {version = "^1.10.2", extras = ["dotenv"]}
|
||||||
|
yarl = "^1.8.1"
|
||||||
|
ujson = "^5.5.0"
|
||||||
|
SQLAlchemy = {version = "^1.4.41", extras = ["mypy", "asyncio"]}
|
||||||
|
asyncpg = {version = "^0.26.0", extras = ["sa"]}
|
||||||
|
redis = {version = "^4.3.4", extras = ["hiredis"]}
|
||||||
|
httptools = "^0.5.0"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
pytest = "^7.1.3"
|
||||||
|
flake8 = "~4.0.1"
|
||||||
|
mypy = "^0.981"
|
||||||
|
isort = "^5.10.1"
|
||||||
|
yesqa = "^1.4.0"
|
||||||
|
pre-commit = "^2.20.0"
|
||||||
|
wemake-python-styleguide = "^0.17.0"
|
||||||
|
black = "^22.8.0"
|
||||||
|
autoflake = "^1.6.1"
|
||||||
|
SQLAlchemy = {version = "^1.4.41", extras = ["mypy"]}
|
||||||
|
pytest-cov = "^4.0.0"
|
||||||
|
anyio = "^3.6.1"
|
||||||
|
pytest-env = "^0.6.2"
|
||||||
|
fakeredis = "^1.9.3"
|
||||||
|
httpx = "^0.23.0"
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
multi_line_output = 3
|
||||||
|
src_paths = ["med_backend",]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
ignore_missing_imports = true
|
||||||
|
allow_subclassing_any = true
|
||||||
|
allow_untyped_calls = true
|
||||||
|
pretty = true
|
||||||
|
show_error_codes = true
|
||||||
|
implicit_reexport = true
|
||||||
|
allow_untyped_decorators = true
|
||||||
|
warn_unused_ignores = false
|
||||||
|
warn_return_any = false
|
||||||
|
namespace_packages = true
|
||||||
|
plugins = ["sqlalchemy.ext.mypy.plugin"]
|
||||||
|
|
||||||
|
# Remove this and add `types-redis`
|
||||||
|
# when the issue https://github.com/python/typeshed/issues/8242 is resolved.
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
'redis.asyncio'
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
filterwarnings = [
|
||||||
|
"error",
|
||||||
|
"ignore::DeprecationWarning",
|
||||||
|
"ignore:.*unclosed.*:ResourceWarning",
|
||||||
|
]
|
||||||
|
env = [
|
||||||
|
"MED_BACKEND_DB_BASE=med_backend_test",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
Loading…
Reference in New Issue
Block a user