Compare commits

...

5 Commits

16 changed files with 1025 additions and 3 deletions

5
.gitignore vendored
View File

@ -107,3 +107,8 @@ ENV/
# Pycharm
.idea/
.idea/vcs.xml
# Sqlite database files
**.db
**.db-shm
**.db-wal

180
poetry.lock generated
View File

@ -201,7 +201,7 @@ clikit = ">=0.6.0,<0.7.0"
name = "click"
version = "8.0.3"
description = "Composable command line interface toolkit"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
@ -225,7 +225,7 @@ pylev = ">=1.3,<2.0"
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@ -311,6 +311,36 @@ python-versions = ">=3.7"
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
[[package]]
name = "flask"
version = "2.0.2"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
click = ">=7.1.2"
itsdangerous = ">=2.0"
Jinja2 = ">=3.0"
Werkzeug = ">=2.0"
[package.extras]
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-assets"
version = "2.0"
description = "Asset management for Flask, to compress and merge CSS and Javascript files."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
Flask = ">=0.8"
webassets = ">=2.0"
[[package]]
name = "gitdb"
version = "4.0.9"
@ -424,6 +454,14 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "itsdangerous"
version = "2.0.1"
description = "Safely pass data to untrusted environments and back."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "jedi"
version = "0.18.1"
@ -451,6 +489,20 @@ python-versions = ">=3.6"
test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"]
trio = ["trio", "async-generator"]
[[package]]
name = "jinja2"
version = "3.0.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "keyring"
version = "21.8.0"
@ -503,6 +555,14 @@ linkify = ["linkify-it-py (>=1.0,<2.0)"]
rtd = ["myst-nb (>=0.11.1,<0.12.0)", "sphinx-book-theme", "sphinx-panels (>=0.4.0,<0.5.0)", "sphinx-copybutton", "sphinx (>=2,<4)", "pyyaml"]
testing = ["coverage", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions", "pytest-benchmark (>=3.2,<4.0)", "psutil"]
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "matplotlib-inline"
version = "0.1.3"
@ -1182,6 +1242,14 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "webassets"
version = "2.0"
description = "Media asset management for Python, with glue code for various web frameworks"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "webencodings"
version = "0.5.1"
@ -1190,6 +1258,17 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "werkzeug"
version = "2.0.3"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
watchdog = ["watchdog"]
[[package]]
name = "wrapt"
version = "1.13.3"
@ -1201,7 +1280,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "34a315e8100872c11d5de1aabd6411adef473a95c3cb9d63451482ca6067875a"
content-hash = "4f4b096207885bcf88a3a525c18e8304f71769e277314dc8e4cd46b8ec7e8edb"
[metadata.files]
appnope = [
@ -1438,6 +1517,14 @@ filelock = [
{file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"},
{file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"},
]
flask = [
{file = "Flask-2.0.2-py3-none-any.whl", hash = "sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a"},
{file = "Flask-2.0.2.tar.gz", hash = "sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2"},
]
flask-assets = [
{file = "Flask-Assets-2.0.tar.gz", hash = "sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2"},
{file = "Flask_Assets-2.0-py3-none-any.whl", hash = "sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"},
]
gitdb = [
{file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
{file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
@ -1470,6 +1557,10 @@ isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
itsdangerous = [
{file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"},
{file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"},
]
jedi = [
{file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
{file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
@ -1478,6 +1569,10 @@ jeepney = [
{file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
{file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
]
jinja2 = [
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
]
keyring = [
{file = "keyring-21.8.0-py3-none-any.whl", hash = "sha256:4be9cbaaaf83e61d6399f733d113ede7d1c73bc75cb6aeb64eee0f6ac39b30ea"},
{file = "keyring-21.8.0.tar.gz", hash = "sha256:1746d3ac913d449a090caf11e9e4af00e26c3f7f7e81027872192b2398b98675"},
@ -1529,6 +1624,77 @@ markdown-it-py = [
{file = "markdown-it-py-0.6.2.tar.gz", hash = "sha256:c3b9f995be0792cbbc8ab2f53d74072eb7ff8a8b622be8d61d38ab879709eca3"},
{file = "markdown_it_py-0.6.2-py3-none-any.whl", hash = "sha256:30b3e9f8198dc82a5df0dcb73fd31d56cd9a43bf8a747feb10b2ba74f962bcb1"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
matplotlib-inline = [
{file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
{file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
@ -1872,10 +2038,18 @@ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
webassets = [
{file = "webassets-2.0-py3-none-any.whl", hash = "sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"},
{file = "webassets-2.0.tar.gz", hash = "sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd"},
]
webencodings = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
]
werkzeug = [
{file = "Werkzeug-2.0.3-py3-none-any.whl", hash = "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8"},
{file = "Werkzeug-2.0.3.tar.gz", hash = "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"},
]
wrapt = [
{file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"},
{file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"},

View File

@ -16,6 +16,8 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
Flask = "^2.0.2"
Flask-Assets = "^2.0"
peewee = "^3.14.8"
peewee-plus = "^1.1.0"

32
section7/__main__.py Normal file
View File

@ -0,0 +1,32 @@
"""Development server stub entrypoint
Flask comes with a built-in development server. This entrypoint allows ``section7``
to be run directly to run the development server and expose some simple config options
for ease of access. Run the below command to start the server:
::
python -m section7
.. warning:: As the development server will tell you on startup, do not use this for
production deployments.
"""
import argparse
from section7.application import APPLICATION
def main():
"""Run the werkzeug development server"""
parser = argparse.ArgumentParser()
parser.add_argument("-b", "--bind", help="Host to listen on", default="127.0.0.1")
parser.add_argument(
"-p", "--port", help="Port to listen on", default=5000, type=int
)
parser.add_argument("-d", "--debug", help="Run in debug mode", action="store_true")
args = parser.parse_args()
APPLICATION.run(host=args.bind, port=args.port, debug=args.debug, load_dotenv=True)
if __name__ == "__main__":
main()

33
section7/application.py Normal file
View File

@ -0,0 +1,33 @@
import flask
import flask_assets
from section7 import configuration
from section7 import database
from section7 import filters
from section7 import router
class Section7Flask(flask.Flask):
"""Stub class to typehint the ``section7`` attribute with the application config"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.section7 = configuration.load()
APPLICATION = Section7Flask(__name__)
assets = flask_assets.Environment(APPLICATION)
# Register the global application handler
APPLICATION.register_error_handler(Exception, router.handle_error)
# Initialize the database connection
APPLICATION.before_first_request(database.initialize)
# Inject the custom jinja2 filters so that they're available in the templates
for custom_filter in filters.FILTERS:
APPLICATION.jinja_env.filters[custom_filter.__name__] = custom_filter
# Attach the application route endpoints
for route in router.ROUTES:
APPLICATION.route(route.route, methods=list(route.methods))(route.entrypoint)

111
section7/configuration.py Normal file
View File

@ -0,0 +1,111 @@
import os
from dataclasses import dataclass
from dataclasses import field
from pathlib import Path
from typing import Optional
from section7 import constants
@dataclass
class DatabaseSqliteConfig:
"""SQLite database backend configuration options
:param path: Path to the SQLite database file
"""
path: Path = Path.cwd() / "section7.db"
@classmethod
def from_env(cls):
"""Build dataclass from environment"""
return cls(
path=Path(os.environ.get("SECTION7_DATABASE_SQLITE_PATH", cls.path))
.expanduser()
.resolve(),
)
@dataclass
class DatabaseMariaConfig:
"""MariaDB database backend configuration options
:param hostname: Hostname or IP address of the host running the database server
:param username: Username of the account to use for connecting to the database server
:param password: Password for the account to use for connecting to the database server
:param port: Port on the host that the database server is listening on
:param schema: Database schema that the application should use
"""
hostname: str = "localhost"
username: str = "root"
password: Optional[str] = None
port: int = 3306
schema: str = "section7"
@classmethod
def from_env(cls):
"""Build dataclass from environment"""
return cls(
hostname=os.getenv("SECTION7_DATABASE_MARIADB_HOSTNAME", cls.hostname),
username=os.getenv("SECTION7_DATABASE_MARIADB_USERNAME", cls.username),
password=os.environ.get("SECTION7_DATABASE_MARIADB_PASSWORD", cls.password),
port=int(os.environ.get("SECTION7_DATABASE_MARIADB_PORT", cls.port)),
schema=os.getenv("SECTION7_DATABASE_MARIADB_SCHEMA", cls.schema),
)
@dataclass
class DatabaseConfig:
"""Database backend configuration
:param backend: Enum selecting the backend to use for storing data
:param sqlite: Container of SQLite settings
:param mariadb: Container of MariaDB settings
"""
backend: constants.DatabaseBackend = constants.DatabaseBackend.SQLITE
sqlite: DatabaseSqliteConfig = field(default_factory=DatabaseSqliteConfig.from_env)
mariadb: DatabaseMariaConfig = field(default_factory=DatabaseMariaConfig.from_env)
@classmethod
def from_env(cls):
"""Build dataclass from environment"""
return cls(
backend=constants.DatabaseBackend[
os.environ["SECTION7_DATABASE_BACKEND"].upper()
]
if "SECTION7_DATABASE_BACKEND" in os.environ
else cls.backend,
)
@dataclass
class Section7Config:
"""Global application configuration settings
:param database: Container of database backend settings
:param contact_email: Public administrative contact email for the site
"""
database: DatabaseConfig = field(default_factory=DatabaseConfig.from_env)
contact_email: str = "admin@enp.one"
@classmethod
def from_env(cls):
"""Build dataclass from environment"""
return cls(contact_email=os.getenv("SECTION7_CONTACT_EMAIL", cls.contact_email))
def load() -> Section7Config:
"""Load the application configuration from environment variables
:returns: Populated environment configuration
"""
try:
return Section7Config.from_env()
except (ValueError, TypeError, IndexError, KeyError) as err:
raise RuntimeError("Failed to load application configuration") from err

282
section7/constants.py Normal file
View File

@ -0,0 +1,282 @@
"""Application constants"""
import enum
class DatabaseBackend(enum.Enum):
"""Enum of supported database backends"""
MARIADB = enum.auto()
SQLITE = enum.auto()
class PayRate(enum.Enum):
"""Enum of payment interval options"""
ANNUALY = enum.auto()
QUARTERLY = enum.auto()
MONTHLY = enum.auto()
WEEKLY = enum.auto()
DAILY = enum.auto()
HOURLY = enum.auto()
OTHER = enum.auto()
class Industry(enum.Enum):
"""Enum of industires records can be associated with"""
class Currency(enum.Enum):
"""Enum of currency options"""
class Country(enum.Enum):
"""Enum of country options"""
AF = "Afghanistan"
AX = "Ã…land Islands"
AL = "Albania"
DZ = "Algeria"
AS = "American Samoa"
AD = "Andorra"
AO = "Angola"
AI = "Anguilla"
AQ = "Antarctica"
AG = "Antigua and Barbuda"
AR = "Argentina"
AM = "Armenia"
AW = "Aruba"
AU = "Australia"
AT = "Austria"
AZ = "Azerbaijan"
BS = "Bahamas"
BH = "Bahrain"
BD = "Bangladesh"
BB = "Barbados"
BY = "Belarus"
BE = "Belgium"
BZ = "Belize"
BJ = "Benin"
BM = "Bermuda"
BT = "Bhutan"
BO = "Plurinational State of Bolivia"
BQ = "Sint Eustatius and Saba Bonaire"
BA = "Bosnia and Herzegovina"
BW = "Botswana"
BV = "Bouvet Island"
BR = "Brazil"
IO = "British Indian Ocean Territory"
BN = "Brunei Darussalam"
BG = "Bulgaria"
BF = "Burkina Faso"
BI = "Burundi"
KH = "Cambodia"
CM = "Cameroon"
CA = "Canada"
CV = "Cape Verde"
KY = "Cayman Islands"
CF = "Central African Republic"
TD = "Chad"
CL = "Chile"
CN = "China"
CX = "Christmas Island"
CC = "Cocos (Keeling) Islands"
CO = "Colombia"
KM = "Comoros"
CG = "Congo"
CD = "the Democratic Republic of the Congo"
CK = "Cook Islands"
CR = "Costa Rica"
CI = "´te d'Ivoire"
HR = "Croatia"
CU = "Cuba"
CW = "Curaçao"
CY = "Cyprus"
CZ = "Czech Republic"
DK = "Denmark"
DJ = "Djibouti"
DM = "Dominica"
DO = "Dominican Republic"
EC = "Ecuador"
EG = "Egypt"
SV = "El Salvador"
GQ = "Equatorial Guinea"
ER = "Eritrea"
EE = "Estonia"
ET = "Ethiopia"
FK = "Falkland Islands (Malvinas)"
FO = "Faroe Islands"
FJ = "Fiji"
FI = "Finland"
FR = "France"
GF = "French Guiana"
PF = "French Polynesia"
TF = "French Southern Territories"
GA = "Gabon"
GM = "Gambia"
GE = "Georgia"
DE = "Germany"
GH = "Ghana"
GI = "Gibraltar"
GR = "Greece"
GL = "Greenland"
GD = "Grenada"
GP = "Guadeloupe"
GU = "Guam"
GT = "Guatemala"
GG = "Guernsey"
GN = "Guinea"
GW = "Guinea-Bissau"
GY = "Guyana"
HT = "Haiti"
HM = "Heard Island and McDonald Islands"
VA = "Holy See (Vatican City State)"
HN = "Honduras"
HK = "Hong Kong"
HU = "Hungary"
IS = "Iceland"
IN = "India"
ID = "Indonesia"
IR = "Islamic Republic of Iran"
IQ = "Iraq"
IE = "Ireland"
IM = "Isle of Man"
IL = "Israel"
IT = "Italy"
JM = "Jamaica"
JP = "Japan"
JE = "Jersey"
JO = "Jordan"
KZ = "Kazakhstan"
KE = "Kenya"
KI = "Kiribati"
KP = "Democratic People's Republic of Korea"
KR = "Republic of Korea"
KW = "Kuwait"
KG = "Kyrgyzstan"
LA = "Lao People's Democratic Republic"
LV = "Latvia"
LB = "Lebanon"
LS = "Lesotho"
LR = "Liberia"
LY = "Libya"
LI = "Liechtenstein"
LT = "Lithuania"
LU = "Luxembourg"
MO = "Macao"
MK = "the Former Yugoslav Republic of Macedonia"
MG = "Madagascar"
MW = "Malawi"
MY = "Malaysia"
MV = "Maldives"
ML = "Mali"
MT = "Malta"
MH = "Marshall Islands"
MQ = "Martinique"
MR = "Mauritania"
MU = "Mauritius"
YT = "Mayotte"
MX = "Mexico"
FM = "Federated States of Micronesia"
MD = "Republic of Moldova"
MC = "Monaco"
MN = "Mongolia"
ME = "Montenegro"
MS = "Montserrat"
MA = "Morocco"
MZ = "Mozambique"
MM = "Myanmar"
NA = "Namibia"
NR = "Nauru"
NP = "Nepal"
NL = "Netherlands"
NC = "New Caledonia"
NZ = "New Zealand"
NI = "Nicaragua"
NE = "Niger"
NG = "Nigeria"
NU = "Niue"
NF = "Norfolk Island"
MP = "Northern Mariana Islands"
NO = "Norway"
OM = "Oman"
PK = "Pakistan"
PW = "Palau"
PS = "State of Palestine"
PA = "Panama"
PG = "Papua New Guinea"
PY = "Paraguay"
PE = "Peru"
PH = "Philippines"
PN = "Pitcairn"
PL = "Poland"
PT = "Portugal"
PR = "Puerto Rico"
QA = "Qatar"
RE = "Réunion"
RO = "Romania"
RU = "Russian Federation"
RW = "Rwanda"
BL = "Saint Barthélemy"
SH = "Ascension and Tristan da Cunha Saint Helena"
KN = "Saint Kitts and Nevis"
LC = "Saint Lucia"
MF = "Saint Martin (French part)"
PM = "Saint Pierre and Miquelon"
VC = "Saint Vincent and the Grenadines"
WS = "Samoa"
SM = "San Marino"
ST = "Sao Tome and Principe"
SA = "Saudi Arabia"
SN = "Senegal"
RS = "Serbia"
SC = "Seychelles"
SL = "Sierra Leone"
SG = "Singapore"
SX = "Sint Maarten (Dutch part)"
SK = "Slovakia"
SI = "Slovenia"
SB = "Solomon Islands"
SO = "Somalia"
ZA = "South Africa"
GS = "South Georgia and the South Sandwich Islands"
SS = "South Sudan"
ES = "Spain"
LK = "Sri Lanka"
SD = "Sudan"
SR = "Suriname"
SJ = "Svalbard and Jan Mayen"
SZ = "Swaziland"
SE = "Sweden"
CH = "Switzerland"
SY = "Syrian Arab Republic"
TW = "Province of China Taiwan"
TJ = "Tajikistan"
TZ = "United Republic of Tanzania"
TH = "Thailand"
TL = "Timor-Leste"
TG = "Togo"
TK = "Tokelau"
TO = "Tonga"
TT = "Trinidad and Tobago"
TN = "Tunisia"
TR = "Turkey"
TM = "Turkmenistan"
TC = "Turks and Caicos Islands"
TV = "Tuvalu"
UG = "Uganda"
UA = "Ukraine"
AE = "United Arab Emirates"
GB = "United Kingdom"
US = "United States"
UM = "United States Minor Outlying Islands"
UY = "Uruguay"
UZ = "Uzbekistan"
VU = "Vanuatu"
VE = "Bolivarian Republic of Venezuela"
VN = "Viet Nam"
VG = "British Virgin Islands"
VI = "U.S. Virgin Islands"
WF = "Wallis and Futuna"
EH = "Western Sahara"
YE = "Yemen"
ZM = "Zambia"

108
section7/database.py Normal file
View File

@ -0,0 +1,108 @@
import datetime
import logging
import uuid
from typing import Optional
import peewee
import peewee_plus
from section7 import configuration
from section7 import constants
INTERFACE = peewee.DatabaseProxy()
def initialize(config: Optional[configuration.Section7Config] = None):
"""Initialize the database interface
Defining the database as an
`unconfigured proxy object <http://docs.peewee-orm.com/en/latest/peewee/database.html#setting-the-database-at-run-time>`_
allows it to be configured at runtime based on the config values.
:param config: Populated configuration container object
"""
logger = logging.getLogger(__name__)
config = config or configuration.load()
if config.database.backend == constants.DatabaseBackend.SQLITE:
logger.debug("Using SQLite database backend")
logger.debug(f"Applying SQLite pragmas: {peewee_plus.SQLITE_DEFAULT_PRAGMAS}")
database = peewee.SqliteDatabase(
config.database.sqlite.path, pragmas=peewee_plus.SQLITE_DEFAULT_PRAGMAS
)
elif config.database.backend == constants.DatabaseBackend.MARIADB:
logger.debug("Using MariaDB database backend")
logger.debug(
"Configuring MariaDB:"
f" {config.database.mariadb.username}@{config.database.mariadb.hostname}:{config.database.mariadb.port},"
f" with database '{config.database.mariadb.schema}'"
)
database = peewee.MySQLDatabase(
config.database.mariadb.schema,
host=config.database.mariadb.hostname,
port=config.database.mariadb.port,
user=config.database.mariadb.username,
password=config.database.mariadb.password,
charset="utf8mb4",
)
else:
raise RuntimeError(
f"Invalid storage backend in configuration: {config.database.backend.name}"
)
INTERFACE.initialize(database)
with INTERFACE.atomic():
INTERFACE.create_tables([DisclosureRecord])
class Section7Model(peewee.Model):
"""Base model for defining common fields and attaching database"""
class Meta: # pylint: disable=too-few-public-methods,missing-class-docstring
database = INTERFACE
uuid = peewee.UUIDField(null=False, unique=True, default=uuid.uuid4)
created = peewee.DateTimeField(null=False, default=datetime.datetime.utcnow)
class DisclosureRecord(Section7Model):
"""Record of a user submitted payment disclosure
:param value: The monetary value of the compensation
:param currency: The currency the value is represented in
:param rate: The rate that the compensation is assigned
:param additionals: Mapping of additional, user defined, compensation details
:param recipients: List of email addresses that should receive the compensation disclosure
:param publish: Whether the record should be published in the publicly available dataset
:param age: Optional age of the user
:param gender: Optional gender of the user
:param race: Optional race of the user
:param country: Optional country where the user's job is located
:param city: Optional city where the user's job is located
:param industry: Optional industry that the user works in
:param title: Optional job title of the user's job
:param company: Optional company the user works at
:param years_experience: Optional number of years the user has worked in the inudstry
:param years_at_company: Optional number of years the user has worked at the company
"""
value = peewee.DecimalField(max_digits=20, decimal_places=3)
currency = peewee_plus.EnumField(constants.Currency)
rate = peewee_plus.EnumField(constants.PayRate)
additionals = peewee_plus.JSONField(default={})
recipients = peewee_plus.JSONField(default=[])
publish = peewee.BooleanField()
age = peewee.IntegerField(null=True)
gender = peewee.CharField(null=True)
race = peewee.CharField(null=True)
country = peewee_plus.EnumField(constants.Country, null=True)
city = peewee.CharField(null=True)
industry = peewee_plus.EnumField(constants.Industry, null=True)
title = peewee.CharField(null=True)
years_experience = peewee.IntegerField(null=True)
years_at_company = peewee.IntegerField(null=True)

16
section7/filters.py Normal file
View File

@ -0,0 +1,16 @@
from typing import Callable
from typing import Optional
from typing import Tuple
import flask
def url_for(value: str, filename: Optional[str] = None) -> str:
"""Wrapper around :func:`flask.url_for` to expose the functionality in a template"""
if filename:
return flask.url_for(value, filename=filename)
return flask.url_for(value)
FILTERS: Tuple[Callable, ...] = (url_for,)

View File

@ -0,0 +1,23 @@
from typing import Callable
from typing import NamedTuple
from typing import Sequence
from typing import Tuple
from section7.router.error_handler import handle_error
from section7.router.home import display_home
class Routable(NamedTuple):
"""Structure for storing details for a given route
:param route: String indicating the Flask URL rule to apply to the route entry
:param entrypoint: The callable that Flask should invoke when the route is accessed
:param methods: Sequence of HTTP method verbs that should be accepted by the route
"""
route: str
entrypoint: Callable
methods: Sequence[str] = ("GET",)
ROUTES: Tuple[Routable, ...] = (Routable(route="/", entrypoint=display_home),)

View File

@ -0,0 +1,122 @@
"""Custom error handler and error page rendering endpoint"""
import enum
import logging
import uuid
from dataclasses import dataclass
from dataclasses import field
from typing import Tuple
import flask
import werkzeug
class ErrorColor(enum.Enum):
"""Enum of possible colors for the error page to be rendered with
Colors can be used to indicate the severity of the problem to a user. Generally, red is reserved
for fatal errors (errors that the user cannot fix themselves), orange for user input or
temporary errors (errors that the user can potentially fix themselves), and blue for
informational errors that don't necessarily indicate a problem, but still needs to block user
access.
Names are the intended usage of the color while values are the CSS classes that the page
uses for the corresponding color.
"""
WARNING = "warning"
ERROR = "error"
INFORMATIONAL = "informational"
class ErrorIcon(enum.Enum):
"""Enum of possible icons for the error page
Names are the intended usage of the icon while values are the Font Awesome CSS class structure
for the corresponding icon.
"""
EXCLAMATION = "fas fa-exclamation-triangle"
INFORMATION = "fas fa-exclamation-circle"
@dataclass
class ErrorDetails:
"""Container of details for an error that can be used to render an error page
:param code: The HTTP response code for the error
:param title: Page title/short description of the error
:param description: Long description of the error. Ideally should include an action the user
can take to debug or fix the problem.
:param icon: The icon that should be added to the page
:param color: The color of the page highlights
"""
code: int = 500
title: str = "Internal Server Error"
description: str = "The application encountered an unhandled error while trying to fulfill the request."
icon: ErrorIcon = ErrorIcon.EXCLAMATION
color: ErrorColor = ErrorColor.ERROR
event_id: str = field(default_factory=lambda: uuid.uuid4().hex)
def handle_error(error: Exception) -> Tuple[str, int]:
"""Handle an exception raised elsewhere in the application
:returns: Rendered template of an error page for the raised exception
"""
logger = logging.getLogger(__name__)
# If the application is running in debug mode then we re-raise the exception so that the
# developer gets the helpful browser-rendered stack trace werkzeug provides
if flask.current_app.config["DEBUG"]:
logger.warning(
"Application is running in debug mode, stack trace will be returned to client"
)
raise error
details = ErrorDetails()
if isinstance(error, NotImplementedError):
details.title = "Not Implemented"
details.description = "The functionality for this resource has not yet been implemented. The resource name is reserved for for future use"
details.color = ErrorColor.INFORMATIONAL
details.icon = ErrorIcon.INFORMATION
elif isinstance(error, werkzeug.exceptions.HTTPException):
details.code = error.code
details.title = error.name
details.description = error.description
if 400 <= error.code < 500:
details.color = ErrorColor.WARNING
details.icon = ErrorIcon.INFORMATION
else:
logger.exception(
f"Event ID {details.event_id}: unhandled application error at '{flask.request.full_path}'"
)
logger.error(
f"Event ID {details.event_id}: Error {details.code} {details.description}"
)
def _render_template(
title: str,
error: ErrorDetails,
) -> str:
"""Stub function to type, enumerate, and document the template parameters
:param title: Value of the page title header
:param error: Container of error details about the error to render on the page
"""
return flask.render_template(
"error.html.j2",
title=title,
error=error,
)
return (
_render_template(
title=f"{details.title} ({details.code})",
error=details,
),
details.code,
)

24
section7/router/home.py Normal file
View File

@ -0,0 +1,24 @@
"""Endpoint for displaying the main homepage of the application"""
import flask
def display_home() -> str:
"""Display the home page
:returns: A response rendering the home page template
"""
def _render_template() -> str:
"""Stub function to type, enumerate, and document the template parameters
:param version: Semantic version of the currently running application
:param common_urls: Container of common application URLs used in mutliple templates
:param testers: List of tester names that the logged in user can read to link to for
easy access search access
:param messages: Optional list of Flask flash messages to render as popup notifications
"""
return flask.render_template(
"home.html.j2",
)
return _render_template()

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% block header %}{% endblock %}
</head>
<body>
<div id="preloader"><div class="spinner"><div></div></div></div>
<div id="main" class="grid-x grid-margin-x">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,10 @@
{% extends "base.html.j2" %}
{% from "macros.html.j2" import make_header %}
{% block header %}
{{ make_header(title) }}
{% endblock %}
{% block content %}
{{ error.title }}
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html.j2" %}
{% from "macros.html.j2" import make_header %}
{% block header %}
{{ make_header('Disclose My Pay') }}
{% endblock %}
{% block content %}
Hello world
{% endblock %}

View File

@ -0,0 +1,56 @@
{% macro make_header(title, js_bundle=none, css_bundle=none) %}
<!-- OpenGraph integration meta -->
<meta property="og:title" content="{{ title }}"/>
<meta property="og:url" content="https://enpaul.net/"/>
<meta property='og:site_name' content="Disclose My Pay"/>
<meta property="og:type" content="website"/>
<meta property='og:locale' content="en_US"/>
<meta property="og:image" content="{{ 'static' | url_for('assets/section7.png') }}"/>
<meta property='og:description' content="Share your compensation information privately with your coworkers"/>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="author" content="admin@enp.one"/>
<meta name="description" content="Share your compensation information privately with your coworkers"/>
<title>{{ title }}</title>
<link rel="shortcut icon" href="{{ 'static' | url_for('assets/section7.ico') }}">
<link rel="icon" type="image/ico" href="{{ 'static' | url_for('assets/section7.ico') }}" sizes="32x32">
<link rel="icon" type="image/ico" href="{{ 'static' | url_for('assets/section7.ico') }}" sizes="16x16">
<link rel="icon" type="image/ico" href="{{ 'static' | url_for('assets/section7.ico') }}" sizes="8x8">
<!-- third party includes -->
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/foundation-sites@6.6.3/dist/css/foundation.min.css"
integrity="sha256-ogmFxjqiTMnZhxCqVmcqTvjfe1Y/ec4WaRj/aQPvn+I="
crossorigin="anonymous"
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"
></script>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/foundation-sites@6.6.3/dist/js/foundation.min.js"
integrity="sha256-pRF3zifJRA9jXGv++b06qwtSqX1byFQOLjqa2PTEb2o="
crossorigin="anonymous"
></script>
{% if js_bundle is not none %}{% assets js_bundle %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}{% endif %}
{% if css_bundle is not none %}{% assets css_bundle %}
<link href="{{ ASSET_URL }}" rel="stylesheet">
{% endassets %}{% endif %}
{% endmacro %}