Merge pull request #8 from YunoHost-Apps/testing

master <- testing
This commit is contained in:
Jens Diemer 2021-01-17 13:31:04 +01:00 committed by GitHub
commit b6f970e090
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 2558 additions and 499 deletions

7
.flake8 Normal file
View file

@ -0,0 +1,7 @@
#
# Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428
#
[flake8]
exclude = .pytest_cache, .tox, dist, htmlcov, local_test
ignore = F405
max-line-length = 119

41
.github/workflows/pytest.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: pytest
on:
schedule:
- cron: '0 8 * * *'
push:
jobs:
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 2
matrix:
python-version: [3.9, 3.8, 3.7]
steps:
- uses: actions/checkout@v1
- name: 'Set up Python ${{ matrix.python-version }}'
uses: actions/setup-python@v1
with:
python-version: '${{ matrix.python-version }}'
- name: 'Install package'
run: |
pip3 install poetry
make install
- name: 'List installed packages'
run: |
poetry run pip freeze
- name: 'Run tests with Python v${{ matrix.python-version }}'
run: |
make pytest
- name: 'Upload coverage report'
run: bash <(curl -s https://codecov.io/bash)
- name: 'Run linters'
if: matrix.python-version == '3.8'
run: |
make lint

4
.gitignore vendored
View file

@ -1,8 +1,10 @@
.* .*
!.github !.github
!.editorconfig !.editorconfig
!.flake8
!.gitignore !.gitignore
__pycache__ __pycache__
secret.txt secret.txt
/local_test/ /local_test/
/poetry.lock /coverage.xml
/htmlcov/

View file

@ -1,4 +1,5 @@
SHELL := /bin/bash SHELL := /bin/bash
MAX_LINE_LENGTH := 119
all: help all: help
@ -16,16 +17,38 @@ check-poetry:
fi fi
install-poetry: ## install or update poetry install-poetry: ## install or update poetry
pip3 install -U pip
pip3 install -U poetry pip3 install -U poetry
install: check-poetry ## install project via poetry install: check-poetry ## install project via poetry
poetry install poetry install
update: install-poetry ## update the sources and installation update: install-poetry ## update the sources and installation and generate "conf/requirements.txt"
poetry run pip install -U pip
poetry update poetry update
poetry export -f requirements.txt --output conf/requirements.txt
local-test: check-poetry ## Run local_test.py to run the project locally lint: ## Run code formatters and linter
poetry run ./local_test.py poetry run flynt --fail-on-change --line_length=${MAX_LINE_LENGTH} .
poetry run isort --check-only .
poetry run flake8 .
fix-code-style: ## Fix code formatting
poetry run flynt --line_length=${MAX_LINE_LENGTH} .
poetry run black --verbose --safe --line-length=${MAX_LINE_LENGTH} --skip-string-normalization .
poetry run isort .
tox-listenvs: check-poetry ## List all tox test environments
poetry run tox --listenvs
tox: check-poetry ## Run pytest via tox with all environments
poetry run tox
pytest: install ## Run pytest
poetry run python3 ./run_pytest.py
local-test: install ## Run local_test.py to run the project locally
poetry run python3 ./local_test.py
local-diff-settings: ## Run "manage.py diffsettings" with local test local-diff-settings: ## Run "manage.py diffsettings" with local test
poetry run python3 local_test/opt_yunohost/manage.py diffsettings poetry run python3 local_test/opt_yunohost/manage.py diffsettings

View file

@ -1,9 +1,9 @@
# Django-For-Runners for YunoHost # Django-ForRunners for YunoHost
[![Integration level](https://dash.yunohost.org/integration/django-for-runners.svg)](https://dash.yunohost.org/appci/app/django-for-runners) ![](https://ci-apps.yunohost.org/ci/badges/django-for-runners.status.svg) ![](https://ci-apps.yunohost.org/ci/badges/django-for-runners.maintain.svg) [![Integration level](https://dash.yunohost.org/integration/django-for-runners.svg)](https://dash.yunohost.org/appci/app/django-for-runners) ![](https://ci-apps.yunohost.org/ci/badges/django-for-runners.status.svg) ![](https://ci-apps.yunohost.org/ci/badges/django-for-runners.maintain.svg)
[![Install Django-For-Runners with YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=django-for-runners) [![Install Django-ForRunners with YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=django-for-runners)
> *This package allows you to install Django-For-Runners quickly and simply on a YunoHost server. > *This package allows you to install Django-ForRunners quickly and simply on a YunoHost server.
If you don't have YunoHost, please consult [the guide](https://yunohost.org/#/install) to learn how to install it.* If you don't have YunoHost, please consult [the guide](https://yunohost.org/#/install) to learn how to install it.*
Current status is pre-alpha: This app doesn't work, yet ;) Current status is pre-alpha: This app doesn't work, yet ;)
@ -12,7 +12,7 @@ Pull requests welcome ;)
## Overview ## Overview
Django-For-Runners is a libre web-based management to catalog things including state and location etc. using Python/Django. Django-ForRunners is a libre web-based management to catalog things including state and location etc. using Python/Django.
## Screenshots ## Screenshots
@ -26,7 +26,7 @@ An admin user is created at installation, the login is what you provided at inst
## Settings and upgrades ## Settings and upgrades
Almost everything related to Django-For-Runners's configuration is handled in a `"../conf/ynh_for_runners_settings.py"` file. Almost everything related to Django-ForRunners's configuration is handled in a `"../conf/ynh_for_runners_settings.py"` file.
You can edit the file `$final_path/local_settings.py` to enable or disable features. You can edit the file `$final_path/local_settings.py` to enable or disable features.
# Miscellaneous # Miscellaneous
@ -34,7 +34,7 @@ You can edit the file `$final_path/local_settings.py` to enable or disable featu
## SSO authentication ## SSO authentication
[SSOwat](https://github.com/YunoHost/SSOwat) is fully supported: [SSOwat](https://github.com/YunoHost/SSOwat) is fully supported via [django_ynh](https://github.com/YunoHost-Apps/django_ynh):
* First user (`$YNH_APP_ARG_ADMIN`) will be created as Django's super user * First user (`$YNH_APP_ARG_ADMIN`) will be created as Django's super user
* All new users will be created as normal users * All new users will be created as normal users
@ -45,7 +45,7 @@ You can edit the file `$final_path/local_settings.py` to enable or disable featu
## Links ## Links
* Report a bug about this package: https://github.com/YunoHost-Apps/django-for-runners_ynh * Report a bug about this package: https://github.com/YunoHost-Apps/django-for-runners_ynh
* Report a bug about Django-For-Runners itself: https://github.com/jedie/django-for-runners * Report a bug about Django-ForRunners itself: https://github.com/jedie/django-for-runners
* YunoHost website: https://yunohost.org/ * YunoHost website: https://yunohost.org/
--- ---
@ -99,7 +99,7 @@ drwxr-xr-x 6 django-for-runners django-for-runners 6 Dec 8 08:37 venv
root@yunohost:~# cd /opt/yunohost/django-for-runners/ root@yunohost:~# cd /opt/yunohost/django-for-runners/
root@yunohost:/opt/yunohost/django-for-runners# source venv/bin/activate root@yunohost:/opt/yunohost/django-for-runners# source venv/bin/activate
(venv) root@yunohost:/opt/yunohost/django-for-runners# ./manage.py check (venv) root@yunohost:/opt/yunohost/django-for-runners# ./manage.py check
Django-For-Runners v0.12.0rc2 (Django v2.2.17) Django-ForRunners v0.12.0rc3 (Django v2.2.17)
DJANGO_SETTINGS_MODULE='ynh_django-for-runners_settings' DJANGO_SETTINGS_MODULE='ynh_django-for-runners_settings'
PROJECT_PATH:/opt/yunohost/django-for-runners/venv/lib/python3.7/site-packages PROJECT_PATH:/opt/yunohost/django-for-runners/venv/lib/python3.7/site-packages
BASE_PATH:/opt/yunohost/django-for-runners BASE_PATH:/opt/yunohost/django-for-runners
@ -114,7 +114,7 @@ root@yunohost:~# journalctl --unit=django-for-runners --follow
## local test ## local test
For quicker developing of Django-For-Runners in the context of YunoHost app, For quicker developing of Django-ForRunners in the context of YunoHost app,
it's possible to run the Django developer server with the settings it's possible to run the Django developer server with the settings
and urls made for YunoHost installation. and urls made for YunoHost installation.

View file

@ -1,46 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import sys
def main():
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_for_runners_settings'
parser = argparse.ArgumentParser(
description='Create or update Django super user.'
)
parser.add_argument('--username')
parser.add_argument('--email')
parser.add_argument('--password')
args = parser.parse_args()
username = args.username
email = args.email or ''
password = args.password
import django
django.setup()
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.filter(username=username).first()
if user:
print('Update existing user and set his password.', file=sys.stderr)
user.is_active = True
user.is_staff = True
user.is_superuser = True
user.set_password(password)
user.email = email
user.save()
else:
print('Create new super user', file=sys.stderr)
User.objects.create_superuser(
username=username,
email=email,
password=password
)
if __name__ == '__main__':
main()

View file

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Django-For-Runners application server Description=Django-ForRunners application server
After=redis.service postgresql.service After=redis.service postgresql.service
[Service] [Service]

View file

@ -3,6 +3,7 @@
""" """
import multiprocessing import multiprocessing
bind = '127.0.0.1:__PORT__' bind = '127.0.0.1:__PORT__'
# https://docs.gunicorn.org/en/latest/settings.html#workers # https://docs.gunicorn.org/en/latest/settings.html#workers

View file

@ -5,8 +5,9 @@ import sys
def main(): def main():
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_for_runners_settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)

View file

@ -12,7 +12,7 @@ location __PATH__/static/ {
# expires 30d; # expires 30d;
#} #}
location / { location __PATH__/ {
# https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf # https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf
# this is needed if you have file import via upload enabled # this is needed if you have file import via upload enabled
@ -30,5 +30,5 @@ location / {
proxy_connect_timeout 30; proxy_connect_timeout 30;
proxy_redirect off; proxy_redirect off;
proxy_pass http://127.0.0.1:__PORT__/; proxy_pass http://127.0.0.1:__PORT__;
} }

353
conf/requirements.txt Normal file
View file

@ -0,0 +1,353 @@
autotask==0.5.4; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:d21578ab14adafb0d9be3490a444d261fd2bcdcbb1ee941b28d4b5e5d1a386ad
bleach==3.2.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd \
--hash=sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080
bx-py-utils==20; python_version >= "3.6" and python_full_version < "4.0.0" \
--hash=sha256:8b7d21e89e5cc99e400ab0bd910632c2d79c7b27aa45afaaa65f4dec2274496e \
--hash=sha256:4fc9b3a78bf82a02ee5d823f45af4623f8c3bec360434b4ae4b87d01cc0ff03a
certifi==2020.12.5; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c
chardet==4.0.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
click==7.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \
--hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a
colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or python_version >= "3.7" and python_full_version < "4.0.0" and sys_platform == "win32" and python_full_version >= "3.5.0" \
--hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \
--hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b
colorlog==4.7.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:0a9dcdba6cab68e8a768448b418a858d73c52b37b6e8dea2568296faece393bd \
--hash=sha256:18d05b616438a75762d7d214b9ec3b05d274466c9f3ddd92807e755840c88251
cycler==0.10.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d \
--hash=sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8
defusedxml==0.6.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93 \
--hash=sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5
diff-match-patch==20200713; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:da6f5a01aa586df23dfc89f3827e1cafbb5420be9d87769eeb079ddfd9477a18 \
--hash=sha256:8bf9d9c4e059d917b5c6312bac0c137971a32815ddbda9c682b949f2986b4d34
django-axes==5.12.0; python_version >= "3.7" and python_version < "4.0" and python_full_version < "4.0.0" \
--hash=sha256:c26167f7ca2003df8358eb23537dffb1d97bd9f44ccef70d5c64a7aba2349456 \
--hash=sha256:db4e17fad2e07baa02bb210c5f819fb5137c548e3a5a4330ccc8662bb88d42c7
django-dbbackup==3.3.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8
django-debug-toolbar==3.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2 \
--hash=sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a
django-for-runners==0.12.0rc3; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:5ff9d5b5705a4d431ac27bc863f1718f7a0768088d216c2274dba7dd0d15fb92 \
--hash=sha256:16e7efb59fbd54c72233fc278dffd3a7f68d0601feb9b8c32929bc65c9cf2cb5
django-import-export==2.5.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:c39c003bfc803fb63ba7742562f1667603a4a8d7426261845d75ce8582d40f48 \
--hash=sha256:cf6f3dabdd4f32dcb26e25c7ddcba7aee3168b55d380b0da79f0349afa17c011
django-ipware==3.0.2; python_version >= "3.7" and python_version < "4.0" and python_full_version < "4.0.0" \
--hash=sha256:c7df8e1410a8e5d6b1fbae58728402ea59950f043c3582e033e866f0f0cf5e94
django-processinfo==1.0.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:3cacad233a5428a5cc6292eafbfddd97948d8d1e4f47e47175a7fcdfcee90f12 \
--hash=sha256:08aefdf7285d1eaa595e46570bcc12f2dfd9e24594d524246110614819076b6c
django-redis==4.12.1; python_version >= "3.5" \
--hash=sha256:306589c7021e6468b2656edc89f62b8ba67e8d5a1c8877e2688042263daa7a63 \
--hash=sha256:1133b26b75baa3664164c3f44b9d5d133d1b8de45d94d79f38d1adc5b1d502e5
django-tools==0.48.3; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:08ed2ae606067f49c2c3949055227a826c8b880e5816114925eca386cf1823af \
--hash=sha256:40444fa16b703b7c6960a800ba76aad42472c9aa70040d549a4d91dbb47a5ddb
django-ynh==0.1.4; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:29250e9dd615f83772a7dc50f25706ebe4e8b776f576d1ed43f6c60289ecd26b \
--hash=sha256:35788a7b91a685a80b15f462796fedb604752c2c8957a6edfb0a777346efa66e
django==2.2.17; python_version >= "3.7" and python_full_version < "4.0.0" and python_version < "4.0" \
--hash=sha256:558cb27930defd9a6042133258caf797b2d1dee233959f537e3dc475cb49bd7c \
--hash=sha256:cf5370a4d7765a9dd6d42a7b96b53c74f9446cd38209211304b210fe0404b861
et-xmlfile==1.0.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b
geographiclib==1.50; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:51cfa698e7183792bce27d8fb63ac8e83689cd8170a730bf35e1a5c5bf8849b9 \
--hash=sha256:12bd46ee7ec25b291ea139b17aa991e7ef373e21abd053949b75c0e9ca55c632
geopy==2.1.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:4db8a2b79a2b3358a7d020ea195be639251a831a1b429c0d1b20c9f00c67c788 \
--hash=sha256:892b219413e7955587b029949af3a1949c6fbac9d5ad17b79d850718f6a9550f
gpxpy==1.4.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:0832041899cdfdc5a607291bbef3d73042e16ffcecc3f2cb9631b699db0bb53f
gunicorn==20.0.4; python_version >= "3.4" \
--hash=sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \
--hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626
icdiff==1.9.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:66972dd03318da55280991db375d3ef6b66d948c67af96c1ebdb21587e86655e
idna==2.10; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
jdcal==1.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:1abf1305fce18b4e8aa248cf8fe0c56ce2032392bc64bbd61b5dff2a19ec8bba \
--hash=sha256:472872e096eb8df219c23f2689fc336668bdb43d194094b5cc1707e1640acfc8
kiwisolver==1.3.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9 \
--hash=sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0 \
--hash=sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21 \
--hash=sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05 \
--hash=sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b \
--hash=sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9 \
--hash=sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4 \
--hash=sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0 \
--hash=sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278 \
--hash=sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689 \
--hash=sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8 \
--hash=sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31 \
--hash=sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc \
--hash=sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454 \
--hash=sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72 \
--hash=sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3 \
--hash=sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131 \
--hash=sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de \
--hash=sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18 \
--hash=sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81 \
--hash=sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e \
--hash=sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000 \
--hash=sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598 \
--hash=sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882 \
--hash=sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621 \
--hash=sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54 \
--hash=sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030 \
--hash=sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6 \
--hash=sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d \
--hash=sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3 \
--hash=sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6 \
--hash=sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248
lxml==4.6.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f \
--hash=sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d \
--hash=sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2 \
--hash=sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e \
--hash=sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e \
--hash=sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2 \
--hash=sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe \
--hash=sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388 \
--hash=sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80 \
--hash=sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b \
--hash=sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8 \
--hash=sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780 \
--hash=sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af \
--hash=sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37 \
--hash=sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98 \
--hash=sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d \
--hash=sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf \
--hash=sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939 \
--hash=sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e \
--hash=sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711 \
--hash=sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089 \
--hash=sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01 \
--hash=sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2 \
--hash=sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc \
--hash=sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d \
--hash=sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3 \
--hash=sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644 \
--hash=sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308 \
--hash=sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505 \
--hash=sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a \
--hash=sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931 \
--hash=sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03 \
--hash=sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5 \
--hash=sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a \
--hash=sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75 \
--hash=sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf \
--hash=sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc
markuppy==1.14; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f
matplotlib==3.3.3; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:b2a5e1f637a92bb6f3526cc54cc8af0401112e81ce5cba6368a1b7908f9e18bc \
--hash=sha256:c586ac1d64432f92857c3cf4478cfb0ece1ae18b740593f8a39f2f0b27c7fda5 \
--hash=sha256:9b03722c89a43a61d4d148acfc89ec5bb54cd0fd1539df25b10eb9c5fa6c393a \
--hash=sha256:2c2c5041608cb75c39cbd0ed05256f8a563e144234a524c59d091abbfa7a868f \
--hash=sha256:c092fc4673260b1446b8578015321081d5db73b94533fe4bf9b69f44e948d174 \
--hash=sha256:27c9393fada62bd0ad7c730562a0fecbd3d5aaa8d9ed80ba7d3ebb8abc4f0453 \
--hash=sha256:b8ba2a1dbb4660cb469fe8e1febb5119506059e675180c51396e1723ff9b79d9 \
--hash=sha256:0caa687fce6174fef9b27d45f8cc57cbc572e04e98c81db8e628b12b563d59a2 \
--hash=sha256:b7b09c61a91b742cb5460b72efd1fe26ef83c1c704f666e0af0df156b046aada \
--hash=sha256:6ffd2d80d76df2e5f9f0c0140b5af97e3b87dd29852dcdb103ec177d853ec06b \
--hash=sha256:5111d6d47a0f5b8f3e10af7a79d5e7eb7e73a22825391834734274c4f312a8a0 \
--hash=sha256:a4fe54eab2c7129add75154823e6543b10261f9b65b2abe692d68743a4999f8c \
--hash=sha256:83e6c895d93fdf93eeff1a21ee96778ba65ef258e5d284160f7c628fee40c38f \
--hash=sha256:b26c472847911f5a7eb49e1c888c31c77c4ddf8023c1545e0e8e0367ba74fb15 \
--hash=sha256:09225edca87a79815822eb7d3be63a83ebd4d9d98d5aa3a15a94f4eee2435954 \
--hash=sha256:eb6b6700ea454bb88333d98601e74928e06f9669c1ea231b4c4c666c1d7701b4 \
--hash=sha256:2d31aff0c8184b05006ad756b9a4dc2a0805e94d28f3abc3187e881b6673b302 \
--hash=sha256:d082f77b4ed876ae94a9373f0db96bf8768a7cca6c58fc3038f94e30ffde1880 \
--hash=sha256:e71cdd402047e657c1662073e9361106c6981e9621ab8c249388dfc3ec1de07b \
--hash=sha256:756ee498b9ba35460e4cbbd73f09018e906daa8537fff61da5b5bf8d5e9de5c7 \
--hash=sha256:7ad44f2c74c50567c694ee91c6fa16d67e7c8af6f22c656b80469ad927688457 \
--hash=sha256:3a4c3e9be63adf8e9b305aa58fb3ec40ecc61fd0f8fd3328ce55bc30e7a2aeb0 \
--hash=sha256:746897fbd72bd462b888c74ed35d812ca76006b04f717cd44698cdfc99aca70d \
--hash=sha256:5ed3d3342698c2b1f3651f8ea6c099b0f196d16ee00e33dc3a6fee8cb01d530a \
--hash=sha256:b1b60c6476c4cfe9e5cf8ab0d3127476fd3d5f05de0f343a452badaad0e4bdec
numpy==1.19.5; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff \
--hash=sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea \
--hash=sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea \
--hash=sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140 \
--hash=sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d \
--hash=sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76 \
--hash=sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a \
--hash=sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827 \
--hash=sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f \
--hash=sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f \
--hash=sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c \
--hash=sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080 \
--hash=sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d \
--hash=sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28 \
--hash=sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7 \
--hash=sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d \
--hash=sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e \
--hash=sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c \
--hash=sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94 \
--hash=sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff \
--hash=sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c \
--hash=sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc \
--hash=sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2 \
--hash=sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa \
--hash=sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd \
--hash=sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa \
--hash=sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8 \
--hash=sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371 \
--hash=sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb \
--hash=sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60 \
--hash=sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e \
--hash=sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e \
--hash=sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73 \
--hash=sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4
odfpy==1.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:fc3b8d1bc098eba4a0fda865a76d9d1e577c4ceec771426bcb169a82c5e9dfe0 \
--hash=sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec
openpyxl==3.0.6; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:1a4b3869c2500b5c713e8e28341cdada49ecfcff1b10cd9006945f5bcefc090d \
--hash=sha256:b229112b46e158b910a5d1b270b212c42773d39cab24e8db527f775b82afc041
packaging==20.8; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858 \
--hash=sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093
pillow==8.1.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a \
--hash=sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2 \
--hash=sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174 \
--hash=sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded \
--hash=sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d \
--hash=sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d \
--hash=sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234 \
--hash=sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8 \
--hash=sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17 \
--hash=sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7 \
--hash=sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e \
--hash=sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b \
--hash=sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0 \
--hash=sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a \
--hash=sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d \
--hash=sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae \
--hash=sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59 \
--hash=sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c \
--hash=sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6 \
--hash=sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378 \
--hash=sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7 \
--hash=sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0 \
--hash=sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b \
--hash=sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865 \
--hash=sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9 \
--hash=sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913 \
--hash=sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206 \
--hash=sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9 \
--hash=sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032 \
--hash=sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820 \
--hash=sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5 \
--hash=sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba
pprintpp==0.4.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d \
--hash=sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403
psycopg2-binary==2.8.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0 \
--hash=sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4 \
--hash=sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db \
--hash=sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5 \
--hash=sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25 \
--hash=sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c \
--hash=sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c \
--hash=sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1 \
--hash=sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2 \
--hash=sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152 \
--hash=sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449 \
--hash=sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859 \
--hash=sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550 \
--hash=sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd \
--hash=sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71 \
--hash=sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4 \
--hash=sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb \
--hash=sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da \
--hash=sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2 \
--hash=sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a \
--hash=sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679 \
--hash=sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf \
--hash=sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b \
--hash=sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67 \
--hash=sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66 \
--hash=sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f \
--hash=sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77 \
--hash=sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94 \
--hash=sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729 \
--hash=sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77 \
--hash=sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83 \
--hash=sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52 \
--hash=sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd \
--hash=sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056 \
--hash=sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6
pyparsing==2.4.7; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
--hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1
python-dateutil==2.8.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.3.0" \
--hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \
--hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a
python-stdnum==1.15; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:ff0e4d2b8731711cf2a73d22964139208088b4bd1a3e3b0c4f78d6e823dd8923 \
--hash=sha256:46c05dff2d5ff89b12cc63ab54cca1e4788e4e3087c163a1b7434a53fa926f28
pytz==2020.5; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4 \
--hash=sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5
pyyaml==5.3.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f \
--hash=sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76 \
--hash=sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2 \
--hash=sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c \
--hash=sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2 \
--hash=sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648 \
--hash=sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a \
--hash=sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf \
--hash=sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97 \
--hash=sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee \
--hash=sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d
redis==3.5.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24 \
--hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2
requests==2.25.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
six==1.15.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
--hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259
sqlparse==0.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0 \
--hash=sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8
svgwrite==1.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:4b21652a1d9c543a6bf4f9f2a54146b214519b7540ca60cb99968ad09ef631d0 \
--hash=sha256:e220a4bf189e7e214a55e8a11421d152b5b6fb1dd660c86a8b6b61fe8cc2ac48
tablib==3.0.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:41aa40981cddd7ec4d1fabeae7c38d271601b306386bd05b5c3bcae13e5aeb20 \
--hash=sha256:f83cac08454f225a34a305daa20e2110d5e6335135d505f93bc66583a5f9c10d
urllib3==1.26.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473 \
--hash=sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08
webencodings==0.5.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
xlrd==2.0.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.6.0" \
--hash=sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd \
--hash=sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88
xlwt==1.3.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e \
--hash=sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88

View file

@ -11,9 +11,11 @@
from pathlib import Path as __Path from pathlib import Path as __Path
from django_ynh.secret_key import get_or_create_secret as __get_or_create_secret
from for_runners_project.settings.base import * # noqa from for_runners_project.settings.base import * # noqa
DEBUG = False
DEBUG = False # Don't turn DEBUG on in production!
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -31,40 +33,41 @@ PATH_URL = PATH_URL.strip('/')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
ROOT_URLCONF = 'ynh_urls' # /opt/yunohost/django-for-runners/ynh_urls.py ROOT_URLCONF = 'urls' # /opt/yunohost/django-for-runners/ynh_urls.py
# ----------------------------------------------------------------------------- # Function that will be called to finalize a user profile:
YNH_SETUP_USER = 'setup_user.setup_project_user'
SECRET_KEY = __get_or_create_secret(FINAL_HOME_PATH / 'secret.txt') # /opt/yunohost/$app/secret.txt
INSTALLED_APPS.append('django_ynh')
MIDDLEWARE.insert(
MIDDLEWARE.index('django.contrib.auth.middleware.AuthenticationMiddleware') + 1,
# login a user via HTTP_REMOTE_USER header from SSOwat:
'django_ynh.sso_auth.auth_middleware.SSOwatRemoteUserMiddleware',
)
# Keep ModelBackend around for per-user permissions and superuser # Keep ModelBackend around for per-user permissions and superuser
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'axes.backends.AxesBackend', # AxesBackend should be the first backend! 'axes.backends.AxesBackend', # AxesBackend should be the first backend!
#
# Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header: # Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header:
'ynh_authenticate.RemoteUserBackend', 'django_ynh.sso_auth.auth_backend.SSOwatUserBackend',
#
# Fallback to normal Django model backend: # Fallback to normal Django model backend:
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
LOGIN_REDIRECT_URL = None LOGIN_REDIRECT_URL = None
LOGIN_URL = '/yunohost/sso/' LOGIN_URL = '/yunohost/sso/'
LOGOUT_REDIRECT_URL = '/yunohost/sso/' LOGOUT_REDIRECT_URL = '/yunohost/sso/'
# /yunohost/sso/?action=logout # /yunohost/sso/?action=logout
# -----------------------------------------------------------------------------
# https://docs.djangoproject.com/en/2.2/howto/auth-remote-user/
# Add RemoteUserMiddleware after AuthenticationMiddleware
MIDDLEWARE.insert(
MIDDLEWARE.index('django.contrib.auth.middleware.AuthenticationMiddleware') + 1,
'ynh_authenticate.RemoteUserMiddleware',
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
ADMINS = ( ADMINS = (('__ADMIN__', '__ADMINMAIL__'),)
('__ADMIN__', '__ADMINMAIL__'),
)
MANAGERS = ADMINS MANAGERS = ADMINS
@ -106,7 +109,7 @@ CACHES = {
'default': { 'default': {
'BACKEND': 'django_redis.cache.RedisCache', 'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/__REDIS_DB__', 'LOCATION': 'redis://127.0.0.1:6379/__REDIS_DB__',
# If redis is running on same host as Django-For-Runners, you might # If redis is running on same host as Django-ForRunners, you might
# want to use unix sockets instead: # want to use unix sockets instead:
# 'LOCATION': 'unix:///var/run/redis/redis.sock?db=1', # 'LOCATION': 'unix:///var/run/redis/redis.sock?db=1',
'OPTIONS': { 'OPTIONS': {
@ -130,7 +133,6 @@ else:
STATIC_ROOT = str(FINAL_WWW_PATH / 'static') STATIC_ROOT = str(FINAL_WWW_PATH / 'static')
MEDIA_ROOT = str(FINAL_WWW_PATH / 'media') MEDIA_ROOT = str(FINAL_WWW_PATH / 'media')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
LOGGING = { LOGGING = {
@ -161,6 +163,7 @@ LOGGING = {
'django': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'django': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False}, 'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False},
'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
'django_ynh': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
'for_runners': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'for_runners': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
}, },
} }

8
conf/setup_user.py Normal file
View file

@ -0,0 +1,8 @@
def setup_project_user(user):
"""
All users used the Django admin, so we need to set the "staff" user flag.
Called from django_ynh.sso_auth
"""
user.is_staff = True
user.save()
return user

18
conf/urls.py Normal file
View file

@ -0,0 +1,18 @@
from django.conf import settings
from django.conf.urls import static
from django.contrib import admin
from django.urls import path
if settings.PATH_URL:
# settings.PATH_URL is the $YNH_APP_ARG_PATH
# Prefix all urls with "PATH_URL":
urlpatterns = [
path(f'{settings.PATH_URL}/', admin.site.urls),
]
if settings.SERVE_FILES:
urlpatterns += static.static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
else:
# Installed to domain root, without a path prefix
# Just use the default project urls.py
from for_runners_project.urls import urlpatterns # noqa

View file

@ -2,7 +2,11 @@
WSGI config WSGI config
""" """
import os import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_for_runners_settings'
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application() os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from django.core.wsgi import get_wsgi_application # noqa
application = get_wsgi_application()

View file

@ -1,185 +0,0 @@
"""
* remote user authentication backend
* remote user middleware
Note: SSOwat/nginx add authentication headers:
'HTTP_AUTHORIZATION': 'Basic XXXXXXXXXXXXXXXX='
'HTTP_AUTH_USER': 'username'
'HTTP_REMOTE_USER': 'username'
Basic auth contains "{username}:{plaintext-password}"
and we get SSOwat cookies like:
'HTTP_COOKIE': 'SSOwAuthUser=username; '
'SSOwAuthHash=593876aa66...99e69f88af1e; '
'SSOwAuthExpire=1609227697.998; '
* Login a user via HTTP_REMOTE_USER header, but check also username in:
* SSOwAuthUser
* HTTP_AUTH_USER
* HTTP_AUTHORIZATION (Basic auth)
* Create new users
* Update Email, First / Last name for existing users
"""
import base64
import logging
from axes.exceptions import AxesBackendPermissionDenied
from django.contrib.auth.backends import RemoteUserBackend as OriginRemoteUserBackend
from django.contrib.auth.middleware import RemoteUserMiddleware as OriginRemoteUserMiddleware
from django.core.exceptions import ValidationError
from inventory.permissions import get_or_create_normal_user_group
logger = logging.getLogger(__name__)
def update_user_profile(request):
"""
Update existing user information:
* Email
* First / Last name
"""
user = request.user
assert user.is_authenticated
update_fields = []
if not user.password:
# Empty password is not valid, so we can't save the model, because of full_clean() call
logger.info('Set unusable password for user: %s', user)
user.set_unusable_password()
update_fields.append('password')
email = request.META.get('HTTP_EMAIL')
if email and user.email != email:
logger.info('Update email: %r -> %r', user.email, email)
user.email = email
update_fields.append('email')
raw_username = request.META.get('HTTP_NAME')
if raw_username:
if ' ' in raw_username:
first_name, last_name = raw_username.split(' ', 1)
else:
first_name = ''
last_name = raw_username
if user.first_name != first_name:
logger.info('Update first name: %r -> %r', user.first_name, first_name)
user.first_name = first_name
update_fields.append('first_name')
if user.last_name != last_name:
logger.info('Update last name: %r -> %r', user.last_name, last_name)
user.last_name = last_name
update_fields.append('last_name')
if update_fields:
try:
user.full_clean()
except ValidationError:
logger.exception('Can not update user: %s', user)
else:
user.save(update_fields=update_fields)
class RemoteUserMiddleware(OriginRemoteUserMiddleware):
"""
Middleware to login a user HTTP_REMOTE_USER header.
Use Django Axes if something is wrong.
Update exising user informations.
"""
header = 'HTTP_REMOTE_USER'
force_logout_if_no_header = True
def process_request(self, request):
# Keep the information if the user is already logged in
was_authenticated = request.user.is_authenticated
super().process_request(request) # login remote user
if not request.user.is_authenticated:
# Not logged in -> nothing to verify here
return
# Check SSOwat cookie informations:
try:
username = request.COOKIES['SSOwAuthUser']
except KeyError:
logger.error('SSOwAuthUser cookie missing!')
# emits a signal indicating user login failed, which is processed by
# axes.signals.log_user_login_failed which logs and flags the failed request.
raise AxesBackendPermissionDenied('Cookie missing')
logger.info('SSOwat username from cookies: %r', username)
if username != request.user.username:
raise AxesBackendPermissionDenied('Wrong username')
# Compare with HTTP_AUTH_USER
try:
username = request.META['HTTP_AUTH_USER']
except KeyError:
logger.error('HTTP_AUTH_USER missing!')
raise AxesBackendPermissionDenied('No HTTP_AUTH_USER')
if username != request.user.username:
raise AxesBackendPermissionDenied('Wrong HTTP_AUTH_USER username')
# Also check 'HTTP_AUTHORIZATION', but only the username ;)
try:
auth = request.META['HTTP_AUTHORIZATION']
except KeyError:
logger.error('HTTP_AUTHORIZATION missing!')
raise AxesBackendPermissionDenied('No HTTP_AUTHORIZATION')
scheme, creds = auth.split(' ', 1)
if scheme.lower() != 'basic':
logger.error('HTTP_AUTHORIZATION with %r not supported', scheme)
raise AxesBackendPermissionDenied('HTTP_AUTHORIZATION scheme not supported')
creds = str(base64.b64decode(creds), encoding='utf-8')
username = creds.split(':', 1)[0]
if username != request.user.username:
raise AxesBackendPermissionDenied('Wrong HTTP_AUTHORIZATION username')
if not was_authenticated:
# First request, after login -> update user informations
logger.info('Remote used was logged in')
update_user_profile(request)
class RemoteUserBackend(OriginRemoteUserBackend):
"""
Authentication backend via SSO/nginx header
"""
create_unknown_user = True
def authenticate(self, request, remote_user):
logger.info('Remote user authenticate: %r', remote_user)
return super().authenticate(request, remote_user)
def configure_user(self, request, user):
"""
Configure a user after creation and return the updated user.
Setup a normal, non-superuser
"""
logger.warning('Configure user %s', user)
user.set_unusable_password() # Always login via SSO
user.is_staff = True
user.is_superuser = False
user.save()
pyinventory_user_group = get_or_create_normal_user_group()[0]
user.groups.set([pyinventory_user_group])
update_user_profile(request)
return user
def user_can_authenticate(self, user):
logger.warning('Remote user login: %s', user)
return True

View file

@ -1,23 +0,0 @@
from django.conf import settings
from django.contrib import admin
from django.urls import path
# def debug_view(request):
# """ debug request.META """
# if not request.user.is_authenticated:
# from django.shortcuts import redirect
# return redirect('admin:index')
#
# import pprint
# meta = pprint.pformat(request.META)
# html = f'<html><body>request.META: <pre>{meta}</pre></body></html>'
# from django.http import HttpResponse
# return HttpResponse(html)
admin.autodiscover()
urlpatterns = [
# path(f'{settings.PATH_URL}/debug/', debug_view),
path(f'{settings.PATH_URL}/', admin.site.urls),
]

149
local_test.py Executable file → Normal file
View file

@ -1,154 +1,29 @@
#!/usr/bin/env python3
""" """
Start Django-For-Runners in YunoHost setup locally. Build a "local_test" YunoHost installation and start the Django dev. server against it.
Note:
You can only run this script, if you are in a activated Django-For-Runners venv! Run via:
make local-test
see README for details ;) see README for details ;)
""" """
import os
import shlex
import subprocess
import sys
from pathlib import Path from pathlib import Path
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_for_runners_settings'
try: try:
import for_runners_project # noqa from django_ynh.local_test import create_local_test
except ImportError as err: except ImportError as err:
raise ImportError( raise ImportError('Did you forget to activate a virtual environment?') from err
'Couldn\'t import Django-For-Runners. Did you '
'forget to activate a virtual environment?'
) from err
BASE_PATH = Path(__file__).parent
BASE_PATH = Path(__file__).parent.absolute()
TEST_PATH = BASE_PATH / 'local_test'
CONF_PATH = BASE_PATH / 'conf'
FINAL_HOME_PATH = TEST_PATH / 'opt_yunohost'
FINAL_WWW_PATH = TEST_PATH / 'var_www'
LOG_FILE = TEST_PATH / 'var_log_django-for-runners.log'
MANAGE_PY_FILE = CONF_PATH / 'manage.py'
CREATE_SUPERUSER_FILE = CONF_PATH / 'create_superuser.py'
SETTINGS_FILE = CONF_PATH / 'ynh_for_runners_settings.py'
URLS_FILE = CONF_PATH / 'ynh_urls.py'
REPLACES = {
'__FINAL_HOME_PATH__': str(FINAL_HOME_PATH),
'__FINAL_WWW_PATH__': str(FINAL_WWW_PATH),
'__LOG_FILE__': str(TEST_PATH / 'var_log_django-for-runners.log'),
'__PATH_URL__': 'app_path',
'__DOMAIN__': '127.0.0.1',
'django.db.backends.postgresql': 'django.db.backends.sqlite3',
"'NAME': '__APP__',": f"'NAME': '{TEST_PATH / 'test_db.sqlite'}',",
'django_redis.cache.RedisCache': 'django.core.cache.backends.dummy.DummyCache',
'DEBUG = False': 'DEBUG = True',
# Just use the default logging setup from Django-For-Runners project:
'LOGGING = {': 'HACKED_DEACTIVATED_LOGGING = {',
}
def verbose_check_call(command, verbose=True, **kwargs):
""" 'verbose' version of subprocess.check_call() """
if verbose:
print('_' * 100)
msg = f'Call: {command!r}'
verbose_kwargs = ', '.join(f'{k}={v!r}' for k, v in sorted(kwargs.items()))
if verbose_kwargs:
msg += f' (kwargs: {verbose_kwargs})'
print(f'{msg}\n', flush=True)
env = os.environ.copy()
env['PYTHONUNBUFFERED'] = '1'
popenargs = shlex.split(command)
subprocess.check_call(
popenargs,
universal_newlines=True,
env=env,
**kwargs
)
def call_manage_py(args):
verbose_check_call(
command=f'{sys.executable} manage.py {args}',
cwd=FINAL_HOME_PATH,
)
def copy_patch(src_file, replaces=None):
dst_file = FINAL_HOME_PATH / src_file.name
print(f'{src_file.relative_to(BASE_PATH)} -> {dst_file.relative_to(BASE_PATH)}')
with src_file.open('r') as f:
content = f.read()
if replaces:
for old, new in replaces.items():
content = content.replace(old, new)
with dst_file.open('w') as f:
f.write(content)
def main(): def main():
print('-' * 100) create_local_test(
django_settings_path=BASE_PATH / 'conf' / 'settings.py',
assert BASE_PATH.is_dir() destination=BASE_PATH / 'local_test',
assert CONF_PATH.is_dir() runserver=True,
assert SETTINGS_FILE.is_file()
assert URLS_FILE.is_file()
for p in (TEST_PATH, FINAL_HOME_PATH, FINAL_WWW_PATH):
if p.is_dir():
print(f'Already exists: "{p.relative_to(BASE_PATH)}", ok.')
else:
print(f'Create: "{p.relative_to(BASE_PATH)}"')
p.mkdir(parents=True, exist_ok=True)
LOG_FILE.touch(exist_ok=True)
# conf/manage.py -> local_test/manage.py
copy_patch(src_file=MANAGE_PY_FILE)
# conf/create_superuser.py -> local_test/opt_yunohost/create_superuser.py
copy_patch(src_file=CREATE_SUPERUSER_FILE)
# conf/ynh_for_runners_settings.py -> local_test/ynh_for_runners_settings.py
copy_patch(src_file=SETTINGS_FILE, replaces=REPLACES)
# conf/ynh_urls.py -> local_test/ynh_urls.py
copy_patch(src_file=URLS_FILE, replaces=REPLACES)
with Path(FINAL_HOME_PATH / 'local_settings.py').open('w') as f:
f.write('# Only for local test run\n')
f.write('SERVE_FILES=True # used in src/for_runners_project/urls.py\n')
# call "local_test/manage.py" via subprocess:
call_manage_py('check --deploy')
call_manage_py('migrate --no-input')
call_manage_py('collectstatic --no-input')
verbose_check_call(
command=f'{sys.executable} create_superuser.py --username="test" --password="test"',
cwd=FINAL_HOME_PATH,
) )
try:
call_manage_py('runserver --nostatic')
except KeyboardInterrupt:
print('\nBye ;)')
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -1,11 +1,11 @@
{ {
"name": "Django-For-Runners", "name": "Django-ForRunners",
"id": "django-for-runners", "id": "django-for-runners",
"packaging_format": 1, "packaging_format": 1,
"description": { "description": {
"en": "Store your GPX tracks of your running (or other sports activity)" "en": "Store your GPX tracks of your running (or other sports activity)"
}, },
"version": "0.12.0rc2~ynh2", "version": "0.12.0rc3~ynh2",
"url": "https://github.com/jedie/django-for-runners", "url": "https://github.com/jedie/django-for-runners",
"license": "GPL-3.0", "license": "GPL-3.0",
"maintainer": { "maintainer": {
@ -26,8 +26,8 @@
"name": "domain", "name": "domain",
"type": "domain", "type": "domain",
"ask": { "ask": {
"en": "Choose a domain for Django-For-Runners", "en": "Choose a domain for Django-ForRunners",
"fr": "Choisissez un domaine pour Django-For-Runners" "fr": "Choisissez un domaine pour Django-ForRunners"
}, },
"example": "domain.org" "example": "domain.org"
}, },
@ -35,8 +35,8 @@
"name": "path", "name": "path",
"type": "path", "type": "path",
"ask": { "ask": {
"en": "Choose a path for Django-For-Runners", "en": "Choose a path for Django-ForRunners",
"fr": "Choisissez un chemin pour Django-For-Runners" "fr": "Choisissez un chemin pour Django-ForRunners"
}, },
"example": "/django-for-runners", "example": "/django-for-runners",
"default": "/django-for-runners" "default": "/django-for-runners"
@ -45,8 +45,8 @@
"name": "admin", "name": "admin",
"type": "user", "type": "user",
"ask": { "ask": {
"en": "Choose an admin user for Django-For-Runners", "en": "Choose an admin user for Django-ForRunners",
"fr": "Choisissez l'administrateur pour Django-For-Runners" "fr": "Choisissez l'administrateur pour Django-ForRunners"
}, },
"example": "johndoe" "example": "johndoe"
}, },
@ -54,8 +54,8 @@
"name": "is_public", "name": "is_public",
"type": "boolean", "type": "boolean",
"ask": { "ask": {
"en": "Should Django-For-Runners be public accessible?", "en": "Should Django-ForRunners be public accessible?",
"fr": "Django-For-Runners doit-il être accessible au public ?" "fr": "Django-ForRunners doit-il être accessible au public ?"
}, },
"help": { "help": {
"en": "Any YunoHost user and anonymous people from the web will be able to access the application", "en": "Any YunoHost user and anonymous people from the web will be able to access the application",

1680
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,92 @@
[tool.poetry] [tool.poetry]
name = "django-for-runners_ynh" name = "django-for-runners_ynh"
version = "0.12.0rc2~ynh2" version = "0.12.0rc3~ynh2"
description = "Test django-for-runners_ynh via local_test.py" description = "Test django-for-runners_ynh via local_test.py"
authors = ["JensDiemer <git@jensdiemer.de>"] authors = ["JensDiemer <git@jensdiemer.de>"]
license = "GPL" license = "GPL"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.7,<4.0.0" python = ">=3.7,<4.0.0"
django-for-runners = "*" django-for-runners = "==0.12.0rc3"
django_ynh = "*"
gunicorn = "*"
psycopg2-binary = "*"
django-redis = "*"
#django-axes = "*" # https://github.com/jazzband/django-axes
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
bx_py_utils = "*"
tox = "*"
pytest = "*"
pytest-cov = "*"
pytest-django = "*"
coveralls = "*"
isort = "*"
flake8 = "*"
flynt = "*"
black = "*"
pyupgrade = "*"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.isort]
# https://pycqa.github.io/isort/docs/configuration/config_files/#pyprojecttoml-preferred-format
atomic=true
line_length=120
case_sensitive=false
skip_glob=["*/htmlcov/*","*/migrations/*"]
multi_line_output=3
include_trailing_comma=true
known_first_party=["inventory"]
no_lines_before="LOCALFOLDER"
default_section="THIRDPARTY"
sections=["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
lines_after_imports=2
[tool.pytest.ini_options]
# https://docs.pytest.org/en/latest/customize.html#pyproject-toml
minversion = "6.0"
norecursedirs = ".* .git __pycache__ conf coverage* dist htmlcov"
# sometimes helpfull "addopts" arguments:
# -vv
# --verbose
# --capture=no
# --trace-config
# --full-trace
# -p no:warnings
addopts = """
--import-mode=importlib
--reuse-db
--nomigrations
--cov=.
--cov-report term-missing
--cov-report html
--cov-report xml
--no-cov-on-fail
--showlocals
--doctest-modules
--failed-first
--last-failed-no-failures all
--new-first
"""
[tool.tox]
# https://tox.readthedocs.io/en/latest/example/basic.html#pyproject-toml-tox-legacy-ini
legacy_tox_ini = """
[tox]
isolated_build = True
envlist = py39,py38,py37
skip_missing_interpreters = True
[testenv]
passenv = *
whitelist_externals = make
commands =
make pytest
"""

25
run_pytest.py Normal file
View file

@ -0,0 +1,25 @@
"""
Run pytest against local test creation
"""
from pathlib import Path
try:
from django_ynh.pytest_helper import run_pytest
except ImportError as err:
raise ImportError('Did you forget to activate a virtual environment?') from err
BASE_PATH = Path(__file__).parent
def main():
run_pytest(
django_settings_path=BASE_PATH / 'conf' / 'settings.py',
destination=BASE_PATH / 'local_test',
)
if __name__ == '__main__':
main()

View file

@ -25,14 +25,7 @@ log_file="${log_path}/django-for-runners.log"
#================================================= #=================================================
# dependencies used by the app # dependencies used by the app
pkg_dependencies="build-essential python3-dev python3-pip python3-venv git \ pkg_dependencies="build-essential python3-dev python3-pip python3-venv git postgresql postgresql-contrib"
postgresql postgresql-contrib"
# Django-For-Runners's version for PIP and settings file
for_runners_version="0.12.0rc2"
# Extra python packages:
pypi_extras="django-redis"
#================================================= #=================================================
# Redis HELPERS # Redis HELPERS
@ -89,4 +82,4 @@ ynh_exec_as() {
else else
sudo -u "$USER" "$@" sudo -u "$USER" "$@"
fi fi
} }

View file

@ -113,13 +113,13 @@ fi
#================================================= #=================================================
# MODIFY SETTINGS # MODIFY SETTINGS
#================================================= #=================================================
ynh_script_progression --message="Modify Django-For-Runners's config file..." ynh_script_progression --message="Modify project config file..."
# save old settings file # save old settings file
settings="$final_path/ynh_for_runners_settings.py" settings="$final_path/settings.py"
ynh_backup_if_checksum_is_different --file="$settings" ynh_backup_if_checksum_is_different --file="$settings"
cp "../conf/ynh_for_runners_settings.py" "$settings" cp "../conf/settings.py" "$settings"
ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings"
ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings"

View file

@ -101,9 +101,10 @@ ynh_system_user_create --username="$app" --home_dir="$final_path" --use_shell
#================================================= #=================================================
# PIP INSTALLATION # PIP INSTALLATION
#================================================= #=================================================
ynh_script_progression --message="Install Django-For-Runners using PIP..." --weight=80 ynh_script_progression --message="Install project via pip..." --weight=80
python3 -m venv "${final_path}/venv" python3 -m venv "${final_path}/venv"
cp ../conf/requirements.txt "$final_path/requirements.txt"
chown -R "$app" "$final_path" chown -R "$app" "$final_path"
#run source in a 'sub shell' #run source in a 'sub shell'
@ -112,18 +113,13 @@ chown -R "$app" "$final_path"
source "${final_path}/venv/bin/activate" source "${final_path}/venv/bin/activate"
set -o nounset set -o nounset
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade setuptools wheel psycopg2-binary ynh_exec_as $app $final_path/venv/bin/pip install -r "$final_path/requirements.txt"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade django-for-runners=="$for_runners_version"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade ${pypi_extras}
) )
#================================================= #=================================================
# copy config files # copy config files
# ================================================ # ================================================
ynh_script_progression --message="Create django-for-runners configuration file..." ynh_script_progression --message="Create project configuration files..."
cp ../conf/create_superuser.py "$final_path/create_superuser.py"
chmod +x "$final_path/create_superuser.py"
gunicorn_conf="$final_path/gunicorn.conf.py" gunicorn_conf="$final_path/gunicorn.conf.py"
cp "../conf/gunicorn.conf.py" "$gunicorn_conf" cp "../conf/gunicorn.conf.py" "$gunicorn_conf"
@ -135,10 +131,8 @@ ynh_store_file_checksum --file="$gunicorn_conf"
cp ../conf/manage.py "$final_path/manage.py" cp ../conf/manage.py "$final_path/manage.py"
chmod +x "$final_path/manage.py" chmod +x "$final_path/manage.py"
cp ../conf/wsgi.py "$final_path/wsgi.py" settings="$final_path/settings.py"
cp "../conf/settings.py" "$settings"
settings="$final_path/ynh_for_runners_settings.py"
cp "../conf/ynh_for_runners_settings.py" "$settings"
ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings"
ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings"
@ -157,10 +151,11 @@ ynh_store_file_checksum --file="$settings"
ynh_app_setting_set --app="$app" --key=redis_db --value="$redis_db" ynh_app_setting_set --app="$app" --key=redis_db --value="$redis_db"
touch "$final_path/local_settings.py" cp ../conf/setup_user.py "$final_path/setup_user.py"
cp ../conf/urls.py "$final_path/urls.py"
cp ../conf/wsgi.py "$final_path/wsgi.py"
cp "../conf/ynh_authenticate.py" "$final_path/ynh_authenticate.py" touch "$final_path/local_settings.py"
cp "../conf/ynh_urls.py" "$final_path/ynh_urls.py"
#================================================= #=================================================
# MIGRATE / COLLECTSTATIC / CREATEADMIN # MIGRATE / COLLECTSTATIC / CREATEADMIN
@ -178,7 +173,9 @@ ynh_script_progression --message="migrate/collectstatic/createadmin..." --weight
./manage.py migrate --no-input ./manage.py migrate --no-input
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input
./create_superuser.py --username="$admin" --email="$admin_mail" --password="django-for-runners"
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
./manage.py create_superuser --username="$admin" --email="$admin_mail"
# Check the configuration # Check the configuration
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later. # This may fail in some cases with errors, etc., but the app works and the user can fix issues later.
@ -233,9 +230,9 @@ then
fi fi
#================================================= #=================================================
# Start django-for-runners via systemd # Start pyinventory via systemd
#================================================= #=================================================
ynh_script_progression --message="Starting Django-For-Runners's services..." --weight=5 ynh_script_progression --message="Starting project services..." --weight=5
ynh_systemd_action --service_name="$app" --action="start" ynh_systemd_action --service_name="$app" --action="start"

View file

@ -83,33 +83,27 @@ ynh_script_progression --message="Configuring a systemd service..."
ynh_add_systemd_config --service="$app" --template="for_runners.service" ynh_add_systemd_config --service="$app" --template="for_runners.service"
#================================================= #=================================================
# UPGRADE PYINVENTORY # UPGRADE VENV
#================================================= #=================================================
ynh_script_progression --message="Upgrade project via pip..." --weight=80
ynh_script_progression --message="Install django-for-runners using PIP..." --weight=15
python3 -m venv "${final_path}/venv" python3 -m venv "${final_path}/venv"
cp ../conf/requirements.txt "$final_path/requirements.txt"
chown -R "$app" "$final_path" chown -R "$app" "$final_path"
#run source in a 'sub shell' #run source in a 'sub shell'
( (
set +o nounset set +o nounset
source "${final_path}/venv/bin/activate" source "${final_path}/venv/bin/activate"
set -o nounset set -o nounset
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade setuptools wheel psycopg2-binary ynh_exec_as $app $final_path/venv/bin/pip install -r "$final_path/requirements.txt"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade django-for-runners=="$for_runners_version"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade ${pypi_extras}
) )
#================================================= #=================================================
# copy config files # copy config files
# ================================================ # ================================================
ynh_script_progression --message="Create django-for-runners configuration file..." ynh_script_progression --message="Create project configuration files..."
ynh_backup_if_checksum_is_different --file="$final_path/create_superuser.py"
cp ../conf/create_superuser.py "$final_path/create_superuser.py"
chmod +x "$final_path/create_superuser.py"
gunicorn_conf="$final_path/gunicorn.conf.py" gunicorn_conf="$final_path/gunicorn.conf.py"
ynh_backup_if_checksum_is_different --file="$gunicorn_conf" ynh_backup_if_checksum_is_different --file="$gunicorn_conf"
@ -123,14 +117,11 @@ ynh_backup_if_checksum_is_different --file="$final_path/manage.py"
cp ../conf/manage.py "$final_path/manage.py" cp ../conf/manage.py "$final_path/manage.py"
chmod +x "$final_path/manage.py" chmod +x "$final_path/manage.py"
ynh_backup_if_checksum_is_different --file="$final_path/wsgi.py"
cp ../conf/wsgi.py "$final_path/wsgi.py"
# save old settings file # save old settings file
settings="$final_path/ynh_for_runners_settings.py" settings="$final_path/settings.py"
ynh_backup_if_checksum_is_different --file="$settings" ynh_backup_if_checksum_is_different --file="$settings"
cp "../conf/ynh_for_runners_settings.py" "$settings" cp "../conf/settings.py" "$settings"
ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings"
ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings"
@ -147,10 +138,16 @@ ynh_replace_string --match_string="__PATH_URL__" --replace_string="$path_url" --
# Recalculate and store the config file checksum into the app settings # Recalculate and store the config file checksum into the app settings
ynh_store_file_checksum --file="$settings" ynh_store_file_checksum --file="$settings"
touch "$final_path/local_settings.py" ynh_backup_if_checksum_is_different --file="$final_path/setup_user.py"
cp ../conf/setup_user.py "$final_path/setup_user.py"
cp "../conf/ynh_authenticate.py" "$final_path/ynh_authenticate.py" ynh_backup_if_checksum_is_different --file="$final_path/urls.py"
cp "../conf/ynh_urls.py" "$final_path/ynh_urls.py" cp ../conf/urls.py "$final_path/urls.py"
ynh_backup_if_checksum_is_different --file="$final_path/wsgi.py"
cp ../conf/wsgi.py "$final_path/wsgi.py"
touch "$final_path/local_settings.py"
#================================================= #=================================================
# MIGRATE PYINVENTORY # MIGRATE PYINVENTORY
@ -168,7 +165,9 @@ ynh_script_progression --message="migrate/collectstatic/createadmin..." --weight
./manage.py migrate --no-input ./manage.py migrate --no-input
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input
./create_superuser.py --username="$admin" --email="$admin_mail" --password="django-for-runners"
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
./manage.py create_superuser --username="$admin" --email="$admin_mail"
# Check the configuration # Check the configuration
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later. # This may fail in some cases with errors, etc., but the app works and the user can fix issues later.
@ -202,9 +201,9 @@ chown -R "$app" "$public_path"
chown -R "$app" "$final_path" chown -R "$app" "$final_path"
#================================================= #=================================================
# Start django-for-runners via systemd # Start pyinventory via systemd
#================================================= #=================================================
ynh_script_progression --message="Starting Django-For-Runners's services..." --weight=5 ynh_script_progression --message="Starting project services..." --weight=5
ynh_systemd_action --service_name="$app" --action="start" ynh_systemd_action --service_name="$app" --action="start"

0
tests/__init__.py Normal file
View file

View file

@ -0,0 +1,145 @@
import for_runners
from axes.models import AccessLog
from bx_py_utils.test_utils.html_assertion import HtmlAssertionMixin
from django.conf import settings
from django.contrib.auth.models import User
from django.test import override_settings
from django.test.testcases import TestCase
from django.urls import NoReverseMatch
from django.urls.base import reverse
from django_ynh.test_utils import generate_basic_auth
from django_ynh.views import request_media_debug_view
@override_settings(DEBUG=False)
class DjangoYnhTestCase(HtmlAssertionMixin, TestCase):
def setUp(self):
super().setUp()
# Always start a fresh session:
self.client = self.client_class()
def test_settings(self):
assert settings.PATH_URL == 'app_path'
assert str(settings.FINAL_HOME_PATH).endswith('/local_test/opt_yunohost')
assert str(settings.FINAL_WWW_PATH).endswith('/local_test/var_www')
assert str(settings.LOG_FILE).endswith('/local_test/var_log_django-for-runners_ynh.log')
assert settings.ROOT_URLCONF == 'urls'
def test_urls(self):
assert reverse('admin:index') == '/app_path/'
# The django_ynh debug view should not be avaiable:
with self.assertRaises(NoReverseMatch):
reverse(request_media_debug_view)
# TODO: https://github.com/jedie/django-for-runners/issues/25
# Serve user uploads via django_tools.serve_media_app:
# assert settings.MEDIA_URL == '/app_path/media/'
# assert reverse('serve_media_app:serve-media', kwargs={'user_token': 'token', 'path': 'foo/bar/'}) == (
# '/app_path/media/token/foo/bar/'
# )
def test_auth(self):
response = self.client.get('/app_path/')
self.assertRedirects(response, expected_url='/app_path/login/?next=/app_path/')
def test_create_unknown_user(self):
assert User.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'test'
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='test',
HTTP_AUTHORIZATION='basic dGVzdDp0ZXN0MTIz',
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
self.assert_html_parts(
response,
parts=(
f'<title>Site administration | Django-ForRunners v{for_runners.__version__}</title>',
'<strong>test</strong>',
),
)
def test_wrong_auth_user(self):
assert User.objects.count() == 0
assert AccessLog.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'test'
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='foobar', # <<< wrong user name
HTTP_AUTHORIZATION='basic dGVzdDp0ZXN0MTIz',
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
assert AccessLog.objects.count() == 1
assert response.status_code == 403 # Forbidden
def test_wrong_cookie(self):
assert User.objects.count() == 0
assert AccessLog.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'foobar' # <<< wrong user name
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='test',
HTTP_AUTHORIZATION='basic dGVzdDp0ZXN0MTIz',
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
assert AccessLog.objects.count() == 1
assert response.status_code == 403 # Forbidden
def test_wrong_authorization_user(self):
assert User.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'test'
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='test',
HTTP_AUTHORIZATION=generate_basic_auth(username='foobar', password='test123'), # <<< wrong user name
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
assert AccessLog.objects.count() == 1
assert response.status_code == 403 # Forbidden

13
tests/test_lint.py Normal file
View file

@ -0,0 +1,13 @@
import shutil
import subprocess
from pathlib import Path
BASE_PATH = Path(__file__).parent.parent
def test_lint():
assert Path(BASE_PATH, 'Makefile').is_file()
make_bin = shutil.which('make')
assert make_bin is not None
subprocess.check_call([make_bin, 'lint'], cwd=BASE_PATH)

View file

@ -0,0 +1,41 @@
import os
import shutil
import subprocess
from pathlib import Path
import for_runners
PACKAGE_ROOT = Path(__file__).parent.parent
def assert_file_contains_string(file_path, string):
with file_path.open('r') as f:
for line in f:
if string in line:
return
raise AssertionError(f'File {file_path} does not contain {string!r} !')
def test_version():
version = for_runners.__version__
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'pyproject.toml'), string=f'version = "{version}~ynh')
assert_file_contains_string(
file_path=Path(PACKAGE_ROOT, 'pyproject.toml'), string=f'django-for-runners = "=={version}"'
)
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'manifest.json'), string=f'"version": "{version}~ynh')
def test_poetry_check():
poerty_bin = shutil.which('poetry')
output = subprocess.check_output(
[poerty_bin, 'check'],
universal_newlines=True,
env=os.environ,
stderr=subprocess.STDOUT,
cwd=str(PACKAGE_ROOT),
)
print(output)
assert output == 'All set!\n'

8
tests/test_utils.py Normal file
View file

@ -0,0 +1,8 @@
from unittest.case import TestCase
from django_ynh.test_utils import generate_basic_auth
class TestUtilsTestCase(TestCase):
def test_generate_basic_auth(self):
assert generate_basic_auth(username='test', password='test123') == 'basic dGVzdDp0ZXN0MTIz'