Merge pull request #9 from kianby/feature-clean-code

Feature clean code
pull/11/head
Yax 2 years ago committed by GitHub
commit 9593244acd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,15 +1,16 @@
all: black test typehint lint all: black test typehint lint
black: black:
isort --multi-line 3 --profile black stacosys/ poetry run isort --multi-line 3 --profile black stacosys/
black stacosys/ poetry run black stacosys/
test: test:
pytest poetry run coverage run -m --source=stacosys pytest
poetry run coverage report
typehint: typehint:
mypy --ignore-missing-imports stacosys/ poetry run mypy --ignore-missing-imports stacosys/
lint: lint:
pylint stacosys/ poetry run pylint stacosys/

@ -6,7 +6,8 @@ db_sqlite_file = db.sqlite
[site] [site]
name = "My blog" name = "My blog"
url = http://blog.mydomain.com proto = https
url = https://blog.mydomain.com
admin_email = admin@mydomain.com admin_email = admin@mydomain.com
redirect = /redirect redirect = /redirect
@ -15,7 +16,6 @@ host = 127.0.0.1
port = 8100 port = 8100
[rss] [rss]
proto = https
file = comments.xml file = comments.xml
[smtp] [smtp]

390
poetry.lock generated

@ -1,33 +1,6 @@
[[package]]
name = "apscheduler"
version = "3.9.1.post1"
description = "In-process task scheduler with Cron-like capabilities"
category = "main"
optional = false
python-versions = "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.dependencies]
pytz = "*"
setuptools = ">=0.7"
six = ">=1.4.0"
tzlocal = ">=2.0,<3.0.0 || >=4.0.0"
[package.extras]
asyncio = ["trollius"]
doc = ["sphinx", "sphinx-rtd-theme"]
gevent = ["gevent"]
mongodb = ["pymongo (>=3.0)"]
redis = ["redis (>=3.0)"]
rethinkdb = ["rethinkdb (>=2.4.0)"]
sqlalchemy = ["sqlalchemy (>=0.8)"]
testing = ["mock", "pytest", "pytest-asyncio", "pytest-asyncio (<0.6)", "pytest-cov", "pytest-tornado5"]
tornado = ["tornado (>=4.3)"]
twisted = ["twisted"]
zookeeper = ["kazoo"]
[[package]] [[package]]
name = "astroid" name = "astroid"
version = "2.12.12" version = "2.12.13"
description = "An abstract syntax tree for Python with inference support." description = "An abstract syntax tree for Python with inference support."
category = "dev" category = "dev"
optional = false optional = false
@ -119,17 +92,14 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "5.5" version = "6.5.0"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" python-versions = ">=3.7"
[package.dependencies]
toml = {version = "*", optional = true, markers = "extra == \"toml\""}
[package.extras] [package.extras]
toml = ["toml"] toml = ["tomli"]
[[package]] [[package]]
name = "coveralls" name = "coveralls"
@ -158,14 +128,6 @@ python-versions = ">=3.7"
[package.extras] [package.extras]
graph = ["objgraph (>=1.7.2)"] graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "main"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "docopt" name = "docopt"
version = "0.6.2" version = "0.6.2"
@ -174,34 +136,22 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "filelock"
version = "3.8.0"
description = "A platform independent file lock."
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
[[package]] [[package]]
name = "flake8" name = "flake8"
version = "5.0.4" version = "6.0.0"
description = "the modular source code checker: pep8 pyflakes and co" description = "the modular source code checker: pep8 pyflakes and co"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6.1" python-versions = ">=3.8.1"
[package.dependencies] [package.dependencies]
mccabe = ">=0.7.0,<0.8.0" mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0" pycodestyle = ">=2.10.0,<2.11.0"
pyflakes = ">=2.5.0,<2.6.0" pyflakes = ">=3.0.0,<3.1.0"
[[package]] [[package]]
name = "flake8-black" name = "flake8-black"
version = "0.3.4" version = "0.3.5"
description = "flake8 plugin to call black as a code style validator" description = "flake8 plugin to call black as a code style validator"
category = "dev" category = "dev"
optional = false optional = false
@ -350,7 +300,7 @@ python-versions = "*"
name = "packaging" name = "packaging"
version = "21.3" version = "21.3"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -377,7 +327,7 @@ python-versions = "*"
name = "platformdirs" name = "platformdirs"
version = "2.5.4" version = "2.5.4"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -389,7 +339,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.0.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -397,17 +347,9 @@ python-versions = ">=3.6"
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pycodestyle" name = "pycodestyle"
version = "2.9.1" version = "2.10.0"
description = "Python style guide checker" description = "Python style guide checker"
category = "dev" category = "dev"
optional = false optional = false
@ -415,7 +357,7 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "pyflakes" name = "pyflakes"
version = "2.5.0" version = "3.0.1"
description = "passive checker of Python programs" description = "passive checker of Python programs"
category = "dev" category = "dev"
optional = false optional = false
@ -423,14 +365,14 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "pylint" name = "pylint"
version = "2.15.6" version = "2.15.7"
description = "python code static checker" description = "python code static checker"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7.2" python-versions = ">=3.7.2"
[package.dependencies] [package.dependencies]
astroid = ">=2.12.12,<=2.14.0-dev0" astroid = ">=2.12.13,<=2.14.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = ">=0.2" dill = ">=0.2"
isort = ">=4.2.5,<6" isort = ">=4.2.5,<6"
@ -446,7 +388,7 @@ testutils = ["gitpython (>3)"]
name = "pyparsing" name = "pyparsing"
version = "3.0.9" version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars" description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.6.8" python-versions = ">=3.6.8"
@ -494,25 +436,6 @@ pytest = ">=4.6"
[package.extras] [package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "pytz"
version = "2022.6"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pytz-deprecation-shim"
version = "0.1.0.post0"
description = "Shims to make deprecation of pytz easier"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
[package.dependencies]
tzdata = {version = "*", markers = "python_version >= \"3.6\""}
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.28.1" version = "2.28.1"
@ -531,35 +454,6 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "setuptools"
version = "65.6.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
@ -576,27 +470,6 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "tox"
version = "3.27.1"
description = "tox is a generic virtualenv management and test command line tool"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""}
filelock = ">=3.0.0"
packaging = ">=14"
pluggy = ">=0.12.0"
py = ">=1.4.17"
six = ">=1.14.0"
virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7"
[package.extras]
docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"]
[[package]] [[package]]
name = "types-markdown" name = "types-markdown"
version = "3.4.2.1" version = "3.4.2.1"
@ -613,60 +486,19 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[[package]]
name = "tzdata"
version = "2022.6"
description = "Provider of IANA time zone data"
category = "main"
optional = false
python-versions = ">=2"
[[package]]
name = "tzlocal"
version = "4.2"
description = "tzinfo object for the local timezone"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pytz-deprecation-shim = "*"
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"]
test = ["pytest (>=4.3)", "pytest-mock (>=3.3)"]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.26.12" version = "1.26.13"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.extras] [package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "virtualenv"
version = "20.16.7"
description = "Virtual Python Environment builder"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
distlib = ">=0.3.6,<1"
filelock = ">=3.4.1,<4"
platformdirs = ">=2.4,<3"
[package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
[[package]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "2.2.2" version = "2.2.2"
@ -692,16 +524,12 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "~3.11" python-versions = "~3.11"
content-hash = "94614b000f5848489ed0e32caf25a06df9daa05bc063d0e2fe03a00f6859d27b" content-hash = "c7fd5e51d22b64ab20394250b12819609483e7e825996bb8c62e6bb2bb537951"
[metadata.files] [metadata.files]
apscheduler = [
{file = "APScheduler-3.9.1.post1-py2.py3-none-any.whl", hash = "sha256:c8c618241dbb2785ed5a687504b14cb1851d6f7b5a4edf3a51e39cc6a069967a"},
{file = "APScheduler-3.9.1.post1.tar.gz", hash = "sha256:b2bea0309569da53a7261bfa0ce19c67ddbfe151bda776a6a907579fdbd3eb2a"},
]
astroid = [ astroid = [
{file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, {file = "astroid-2.12.13-py3-none-any.whl", hash = "sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907"},
{file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"}, {file = "astroid-2.12.13.tar.gz", hash = "sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"},
] ]
attrs = [ attrs = [
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
@ -751,58 +579,56 @@ colorama = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
coverage = [ coverage = [
{file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
{file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
{file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
{file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
{file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
{file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
{file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
{file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
{file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
{file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
{file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
{file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
{file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
{file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
{file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
{file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
{file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
{file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
{file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
{file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
{file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
{file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
{file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
{file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
{file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
{file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
{file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
{file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
{file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
{file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
{file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
{file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
{file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
{file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
{file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
{file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
{file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
{file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
{file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
{file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
{file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
{file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
{file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
{file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
{file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
{file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
{file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
{file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
{file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
{file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
{file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
] ]
coveralls = [ coveralls = [
{file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"},
@ -812,24 +638,16 @@ dill = [
{file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"},
{file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"},
] ]
distlib = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
docopt = [ docopt = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
] ]
filelock = [
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
flake8 = [ flake8 = [
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"},
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
] ]
flake8-black = [ flake8-black = [
{file = "flake8-black-0.3.4.tar.gz", hash = "sha256:7f96a4c80a828d09f1d550724e16aabb2adacd6a5f8e0bb051df422fc63d2183"}, {file = "flake8-black-0.3.5.tar.gz", hash = "sha256:9e93252b1314a8eb3c2f55dec54a07239e502b12f57567f2c105f2202714b15e"},
{file = "flake8_black-0.3.4-py3-none-any.whl", hash = "sha256:fb52f258dfa6a25645c4ba8730eadc5f2ecd32057bf6c9fc21aef1cba9fefd74"}, {file = "flake8_black-0.3.5-py3-none-any.whl", hash = "sha256:4948a579fdddd98fbf935fd94255dfcfce560c4ddc1ceee08e3f12d6114c8619"},
] ]
flask = [ flask = [
{file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"},
@ -981,21 +799,17 @@ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
] ]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pycodestyle = [ pycodestyle = [
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
] ]
pyflakes = [ pyflakes = [
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
] ]
pylint = [ pylint = [
{file = "pylint-2.15.6-py3-none-any.whl", hash = "sha256:15060cc22ed6830a4049cf40bc24977744df2e554d38da1b2657591de5bcd052"}, {file = "pylint-2.15.7-py3-none-any.whl", hash = "sha256:1d561d1d3e8be9dd880edc685162fbdaa0409c88b9b7400873c0cf345602e326"},
{file = "pylint-2.15.6.tar.gz", hash = "sha256:25b13ddcf5af7d112cf96935e21806c1da60e676f952efb650130f2a4483421c"}, {file = "pylint-2.15.7.tar.gz", hash = "sha256:91e4776dbcb4b4d921a3e4b6fec669551107ba11f29d9199154a01622e460a57"},
] ]
pyparsing = [ pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
@ -1012,30 +826,10 @@ pytest-cov = [
{file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
{file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
] ]
pytz = [
{file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"},
{file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"},
]
pytz-deprecation-shim = [
{file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"},
{file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"},
]
requests = [ requests = [
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
] ]
setuptools = [
{file = "setuptools-65.6.0-py3-none-any.whl", hash = "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840"},
{file = "setuptools-65.6.0.tar.gz", hash = "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@ -1044,10 +838,6 @@ tomlkit = [
{file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
{file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
] ]
tox = [
{file = "tox-3.27.1-py2.py3-none-any.whl", hash = "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84"},
{file = "tox-3.27.1.tar.gz", hash = "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04"},
]
types-markdown = [ types-markdown = [
{file = "types-Markdown-3.4.2.1.tar.gz", hash = "sha256:03c0904cf5886a7d8193e2f50bcf842afc89e0ab80f060f389f6c2635c65628f"}, {file = "types-Markdown-3.4.2.1.tar.gz", hash = "sha256:03c0904cf5886a7d8193e2f50bcf842afc89e0ab80f060f389f6c2635c65628f"},
{file = "types_Markdown-3.4.2.1-py3-none-any.whl", hash = "sha256:b2333f6f4b8f69af83de359e10a097e4a3f14bbd6d2484e1829d9b0ec56fa0cb"}, {file = "types_Markdown-3.4.2.1-py3-none-any.whl", hash = "sha256:b2333f6f4b8f69af83de359e10a097e4a3f14bbd6d2484e1829d9b0ec56fa0cb"},
@ -1056,21 +846,9 @@ typing-extensions = [
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
] ]
tzdata = [
{file = "tzdata-2022.6-py2.py3-none-any.whl", hash = "sha256:04a680bdc5b15750c39c12a448885a51134a27ec9af83667663f0b3a1bf3f342"},
{file = "tzdata-2022.6.tar.gz", hash = "sha256:91f11db4503385928c15598c98573e3af07e7229181bee5375bd30f1695ddcae"},
]
tzlocal = [
{file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"},
{file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"},
]
urllib3 = [ urllib3 = [
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
]
virtualenv = [
{file = "virtualenv-20.16.7-py3-none-any.whl", hash = "sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29"},
{file = "virtualenv-20.16.7.tar.gz", hash = "sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e"},
] ]
werkzeug = [ werkzeug = [
{file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"},

@ -8,19 +8,17 @@ include = ["run.py"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "~3.11" python = "~3.11"
apscheduler = "^3.6.3"
pyrss2gen = "^1.1" pyrss2gen = "^1.1"
markdown = "^3.1.1" markdown = "^3.1.1"
requests = "^2.25.1" requests = "^2.25.1"
coverage = "^5.5" coverage = "^6.5"
peewee = "^3.14.8" peewee = "^3.14.8"
tox = "^3.24.5"
background = "^0.2.1" background = "^0.2.1"
Flask = "^2.1.1" Flask = "^2.1.1"
types-markdown = "^3.4.2.1" types-markdown = "^3.4.2.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pylint = "^2.15.5" pylint = "^2.15"
mypy = "^0.991" mypy = "^0.991"
pytest = "^7.2.0" pytest = "^7.2.0"
coveralls = "^3.3.1" coveralls = "^3.3.1"

File diff suppressed because it is too large Load Diff

@ -1,43 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import smtplib
import ssl
from email.mime.text import MIMEText
logger = logging.getLogger(__name__)
class Mailer:
def __init__(
self,
smtp_host,
smtp_port,
smtp_login,
smtp_password,
site_admin_email,
):
self._smtp_host = smtp_host
self._smtp_port = smtp_port
self._smtp_login = smtp_login
self._smtp_password = smtp_password
self._site_admin_email = site_admin_email
def send(self, subject, message):
sender = self._smtp_login
receivers = [self._site_admin_email]
msg = MIMEText(message)
msg["Subject"] = subject
msg["To"] = self._site_admin_email
msg["From"] = sender
context = ssl.create_default_context()
# TODO catch SMTP failure
with smtplib.SMTP_SSL(
self._smtp_host, self._smtp_port, context=context
) as server:
server.login(self._smtp_login, self._smtp_password)
server.send_message(msg, sender, receivers)
return True

@ -1,66 +0,0 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from datetime import datetime
import markdown
import PyRSS2Gen
from stacosys.model.comment import Comment
class Rss:
def __init__(
self,
lang,
rss_file,
rss_proto,
site_name,
site_url,
):
self._lang = lang
self._rss_file = rss_file
self._rss_proto = rss_proto
self._site_name = site_name
self._site_url = site_url
def generate(self):
md = markdown.Markdown()
items = []
for row in (
Comment.select()
.where(Comment.published)
.order_by(-Comment.published)
.limit(10)
):
item_link = "%s://%s%s" % (
self._rss_proto,
self._site_url,
row.url,
)
items.append(
PyRSS2Gen.RSSItem(
title="%s - %s://%s%s"
% (
self._rss_proto,
row.author_name,
self._site_url,
row.url,
),
link=item_link,
description=md.convert(row.content),
guid=PyRSS2Gen.Guid("%s/%d" % (item_link, row.id)),
pubDate=row.published,
)
)
rss_title = 'Commentaires du site "%s"' % self._site_name
rss = PyRSS2Gen.RSS2(
title=rss_title,
link="%s://%s" % (self._rss_proto, self._site_url),
description=rss_title,
lastBuildDate=datetime.now(),
items=items,
)
rss.write_xml(open(self._rss_file, "w"), encoding="utf-8")

@ -38,5 +38,6 @@ def query_comments():
@app.route("/api/comments/count", methods=["GET"]) @app.route("/api/comments/count", methods=["GET"])
def get_comments_count(): def get_comments_count():
# TODO process pending comments
url = request.args.get("url", "") url = request.args.get("url", "")
return jsonify({"count": dao.count_published_comments(url)}) return jsonify({"count": dao.count_published_comments(url)})

@ -7,6 +7,8 @@ from flask import abort, redirect, request
from stacosys.db import dao from stacosys.db import dao
from stacosys.interface import app from stacosys.interface import app
from stacosys.service import config, mailer
from stacosys.service.configuration import ConfigParameter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,7 +48,7 @@ def new_form_comment():
# send notification e-mail asynchronously # send notification e-mail asynchronously
submit_new_comment(comment) submit_new_comment(comment)
return redirect(app.config.get("SITE_REDIRECT"), code=302) return redirect(config.get(ConfigParameter.SITE_REDIRECT), code=302)
def check_form_data(posted_comment): def check_form_data(posted_comment):
@ -57,7 +59,7 @@ def check_form_data(posted_comment):
@background.task @background.task
def submit_new_comment(comment): def submit_new_comment(comment):
site_url = app.config.get("SITE_URL") site_url = config.get(ConfigParameter.SITE_URL)
comment_list = ( comment_list = (
f"Web admin interface: {site_url}/web/admin", f"Web admin interface: {site_url}/web/admin",
"", "",
@ -72,17 +74,11 @@ def submit_new_comment(comment):
email_body = "\n".join(comment_list) email_body = "\n".join(comment_list)
# send email to notify admin # send email to notify admin
subject = "STACOSYS " + app.config.get("SITE_NAME") site_name = config.get(ConfigParameter.SITE_NAME)
if app.config.get("MAILER").send(subject, email_body): subject = f"STACOSYS {site_name}"
if mailer.send(subject, email_body):
logger.debug("new comment processed") logger.debug("new comment processed")
# save notification datetime # save notification datetime
dao.notify_comment(comment) dao.notify_comment(comment)
else: else:
logger.warning("rescheduled. send mail failure %s", subject) logger.warning("rescheduled. send mail failure %s", subject)
@background.callback
def submit_new_comment_callback(future):
# TODO use future to log submit status
logger.debug(future)

@ -8,6 +8,8 @@ from flask import flash, redirect, render_template, request, session
from stacosys.db import dao from stacosys.db import dao
from stacosys.interface import app from stacosys.interface import app
from stacosys.service import config, rss
from stacosys.service.configuration import ConfigParameter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,8 +25,8 @@ def index():
def is_login_ok(username, password): def is_login_ok(username, password):
hashed = hashlib.sha256(password.encode()).hexdigest().upper() hashed = hashlib.sha256(password.encode()).hexdigest().upper()
return ( return (
app.config.get("WEB_USERNAME") == username config.get(ConfigParameter.WEB_USERNAME) == username
and app.config.get("WEB_PASSWORD") == hashed and config.get(ConfigParameter.WEB_PASSWORD) == hashed
) )
@ -40,7 +42,7 @@ def login():
flash("Identifiant ou mot de passe incorrect") flash("Identifiant ou mot de passe incorrect")
return redirect("/web/login") return redirect("/web/login")
# GET # GET
return render_template("login_" + app.config.get("LANG", "fr") + ".html") return render_template("login_" + config.get(ConfigParameter.LANG) + ".html")
@app.route("/web/logout", methods=["GET"]) @app.route("/web/logout", methods=["GET"])
@ -51,16 +53,19 @@ def logout():
@app.route("/web/admin", methods=["GET"]) @app.route("/web/admin", methods=["GET"])
def admin_homepage(): def admin_homepage():
if not ("user" in session and session["user"] == app.config.get("WEB_USERNAME")): if not (
"user" in session
and session["user"] == config.get(ConfigParameter.WEB_USERNAME)
):
# TODO localization # TODO localization
flash("Vous avez été déconnecté.") flash("Vous avez été déconnecté.")
return redirect("/web/login") return redirect("/web/login")
comments = dao.find_not_published_comments() comments = dao.find_not_published_comments()
return render_template( return render_template(
"admin_" + app.config.get("LANG", "fr") + ".html", "admin_" + config.get(ConfigParameter.LANG) + ".html",
comments=comments, comments=comments,
baseurl=app.config.get("SITE_URL"), baseurl=config.get(ConfigParameter.SITE_URL),
) )
@ -72,7 +77,7 @@ def admin_action():
flash("Commentaire introuvable") flash("Commentaire introuvable")
elif request.form.get("action") == "APPROVE": elif request.form.get("action") == "APPROVE":
dao.publish_comment(comment) dao.publish_comment(comment)
app.config.get("RSS").generate() rss.generate()
# TODO localization # TODO localization
flash("Commentaire publié") flash("Commentaire publié")
else: else:

@ -6,12 +6,11 @@ import logging
import os import os
import sys import sys
from stacosys.conf.config import Config, ConfigParameter
from stacosys.core.mailer import Mailer
from stacosys.core.rss import Rss
from stacosys.db import database from stacosys.db import database
from stacosys.interface import api, app, form from stacosys.interface import api, app, form
from stacosys.interface.web import admin from stacosys.interface.web import admin
from stacosys.service import config, mailer, rss
from stacosys.service.configuration import ConfigParameter
# configure logging # configure logging
@ -30,23 +29,22 @@ def stacosys_server(config_pathname):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
configure_logging(logging.INFO) configure_logging(logging.INFO)
logging.getLogger("werkzeug").level = logging.WARNING logging.getLogger("werkzeug").level = logging.WARNING
logging.getLogger("apscheduler.executors").level = logging.WARNING
# check config file exists # check config file exists
if not os.path.isfile(config_pathname): if not os.path.isfile(config_pathname):
logger.error("Configuration file '%s' not found.", config_pathname) logger.error("Configuration file '%s' not found.", config_pathname)
sys.exit(1) sys.exit(1)
# load config # load and check config
conf = Config.load(config_pathname) config.load(config_pathname)
is_config_ok, erreur_config = conf.check() is_config_ok, erreur_config = config.check()
if not is_config_ok: if not is_config_ok:
logger.error("Configuration incorrecte '%s'", erreur_config) logger.error("Configuration incorrecte '%s'", erreur_config)
sys.exit(1) sys.exit(1)
logger.info(conf) logger.info(config)
# check database file exists (prevents from creating a fresh db) # check database file exists (prevents from creating a fresh db)
db_pathname = conf.get(ConfigParameter.DB_SQLITE_FILE) db_pathname = config.get(ConfigParameter.DB_SQLITE_FILE)
if not db_pathname or not os.path.isfile(db_pathname): if not db_pathname or not os.path.isfile(db_pathname):
logger.error("Database file '%s' not found.", db_pathname) logger.error("Database file '%s' not found.", db_pathname)
sys.exit(1) sys.exit(1)
@ -57,39 +55,30 @@ def stacosys_server(config_pathname):
logger.info("Start Stacosys application") logger.info("Start Stacosys application")
# generate RSS # generate RSS
rss = Rss( rss.configure(
conf.get(ConfigParameter.LANG), config.get(ConfigParameter.RSS_FILE),
conf.get(ConfigParameter.RSS_FILE), config.get(ConfigParameter.SITE_NAME),
conf.get(ConfigParameter.RSS_PROTO), config.get(ConfigParameter.SITE_PROTO),
conf.get(ConfigParameter.SITE_NAME), config.get(ConfigParameter.SITE_URL),
conf.get(ConfigParameter.SITE_URL),
) )
rss.generate() rss.generate()
# configure mailer # configure mailer
mailer = Mailer( mailer.configure_smtp(
conf.get(ConfigParameter.SMTP_HOST), config.get(ConfigParameter.SMTP_HOST),
conf.get_int(ConfigParameter.SMTP_PORT), config.get_int(ConfigParameter.SMTP_PORT),
conf.get(ConfigParameter.SMTP_LOGIN), config.get(ConfigParameter.SMTP_LOGIN),
conf.get(ConfigParameter.SMTP_PASSWORD), config.get(ConfigParameter.SMTP_PASSWORD),
conf.get(ConfigParameter.SITE_ADMIN_EMAIL),
) )
mailer.configure_destination(config.get(ConfigParameter.SITE_ADMIN_EMAIL))
mailer.check()
# inject config parameters into flask
app.config.update(LANG=conf.get(ConfigParameter.LANG))
app.config.update(SITE_NAME=conf.get(ConfigParameter.SITE_NAME))
app.config.update(SITE_URL=conf.get(ConfigParameter.SITE_URL))
app.config.update(SITE_REDIRECT=conf.get(ConfigParameter.SITE_REDIRECT))
app.config.update(WEB_USERNAME=conf.get(ConfigParameter.WEB_USERNAME))
app.config.update(WEB_PASSWORD=conf.get(ConfigParameter.WEB_PASSWORD))
app.config.update(MAILER=mailer)
app.config.update(RSS=rss)
logger.info("start interfaces %s %s %s", api, form, admin) logger.info("start interfaces %s %s %s", api, form, admin)
# start Flask # start Flask
app.run( app.run(
host=conf.get(ConfigParameter.HTTP_HOST), host=config.get(ConfigParameter.HTTP_HOST),
port=conf.get_int(ConfigParameter.HTTP_PORT), port=config.get_int(ConfigParameter.HTTP_PORT),
debug=False, debug=False,
use_reloader=False, use_reloader=False,
) )

@ -0,0 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from .configuration import Config
from .mail import Mailer
from .rssfeed import Rss
config = Config()
mailer = Mailer()
rss = Rss()

@ -12,7 +12,6 @@ class ConfigParameter(Enum):
HTTP_HOST = "http.host" HTTP_HOST = "http.host"
HTTP_PORT = "http.port" HTTP_PORT = "http.port"
RSS_PROTO = "rss.proto"
RSS_FILE = "rss.file" RSS_FILE = "rss.file"
SMTP_HOST = "smtp.host" SMTP_HOST = "smtp.host"
@ -20,6 +19,7 @@ class ConfigParameter(Enum):
SMTP_LOGIN = "smtp.login" SMTP_LOGIN = "smtp.login"
SMTP_PASSWORD = "smtp.password" SMTP_PASSWORD = "smtp.password"
SITE_PROTO = "site.proto"
SITE_NAME = "site.name" SITE_NAME = "site.name"
SITE_URL = "site.url" SITE_URL = "site.url"
SITE_ADMIN_EMAIL = "site.admin_email" SITE_ADMIN_EMAIL = "site.admin_email"
@ -30,14 +30,16 @@ class ConfigParameter(Enum):
class Config: class Config:
def __init__(self):
self._cfg = configparser.ConfigParser()
@classmethod _cfg = configparser.ConfigParser()
def load(cls, config_pathname):
config = cls() # def __new__(cls):
config._cfg.read(config_pathname) # if not hasattr(cls, "instance"):
return config # cls.instance = super(Config, cls).__new__(cls)
# return cls.instance
def load(self, config_pathname):
self._cfg.read(config_pathname)
def _split_key(self, key: ConfigParameter): def _split_key(self, key: ConfigParameter):
section, param = str(key.value).split(".") section, param = str(key.value).split(".")
@ -50,12 +52,12 @@ class Config:
section, param = self._split_key(key) section, param = self._split_key(key)
return self._cfg.has_option(section, param) return self._cfg.has_option(section, param)
def get(self, key: ConfigParameter): def get(self, key: ConfigParameter) -> str:
section, param = self._split_key(key) section, param = self._split_key(key)
return ( return (
self._cfg.get(section, param) self._cfg.get(section, param)
if self._cfg.has_option(section, param) if self._cfg.has_option(section, param)
else None else ""
) )
def put(self, key: ConfigParameter, value): def put(self, key: ConfigParameter, value):
@ -64,11 +66,11 @@ class Config:
self._cfg.add_section(section) self._cfg.add_section(section)
self._cfg.set(section, param, str(value)) self._cfg.set(section, param, str(value))
def get_int(self, key: ConfigParameter): def get_int(self, key: ConfigParameter) -> int:
value = self.get(key) value = self.get(key)
return int(value) if value else 0 return int(value) if value else 0
def get_bool(self, key: ConfigParameter): def get_bool(self, key: ConfigParameter) -> bool:
value = self.get(key) value = self.get(key)
assert value in ( assert value in (
"yes", "yes",
@ -85,8 +87,8 @@ class Config:
return (True, None) return (True, None)
def __repr__(self): def __repr__(self):
d = dict() dict_repr = {}
for section in self._cfg.sections(): for section in self._cfg.sections():
for option in self._cfg.options(section): for option in self._cfg.options(section):
d[".".join([section, option])] = self._cfg.get(section, option) dict_repr[".".join([section, option])] = self._cfg.get(section, option)
return str(d) return str(dict_repr)

@ -0,0 +1,62 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import smtplib
import ssl
from email.mime.text import MIMEText
logger = logging.getLogger(__name__)
class Mailer:
def __init__(self) -> None:
self._smtp_host: str = ""
self._smtp_port: int = 0
self._smtp_login: str = ""
self._smtp_password: str = ""
self._site_admin_email: str = ""
def configure_smtp(
self,
smtp_host,
smtp_port,
smtp_login,
smtp_password,
) -> None:
self._smtp_host = smtp_host
self._smtp_port = smtp_port
self._smtp_login = smtp_login
self._smtp_password = smtp_password
def configure_destination(self, site_admin_email) -> None:
self._site_admin_email = site_admin_email
def check(self):
server = smtplib.SMTP_SSL(
self._smtp_host, self._smtp_port, context=ssl.create_default_context()
)
server.login(self._smtp_login, self._smtp_password)
server.close()
def send(self, subject, message) -> bool:
sender = self._smtp_login
receivers = [self._site_admin_email]
msg = MIMEText(message)
msg["Subject"] = subject
msg["To"] = self._site_admin_email
msg["From"] = sender
# pylint: disable=bare-except
try:
server = smtplib.SMTP_SSL(
self._smtp_host, self._smtp_port, context=ssl.create_default_context()
)
server.login(self._smtp_login, self._smtp_password)
server.send_message(msg, sender, receivers)
server.close()
success = True
except:
success = False
return success

@ -0,0 +1,63 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from datetime import datetime
import markdown
import PyRSS2Gen
from stacosys.model.comment import Comment
class Rss:
def __init__(self) -> None:
self._rss_file: str = ""
self._site_proto: str = ""
self._site_name: str = ""
self._site_url: str = ""
def configure(
self,
rss_file,
site_name,
site_proto,
site_url,
) -> None:
self._rss_file = rss_file
self._site_name = site_name
self._site_proto = site_proto
self._site_url = site_url
def generate(self) -> None:
markdownizer = markdown.Markdown()
items = []
for row in (
Comment.select()
.where(Comment.published)
.order_by(-Comment.published)
.limit(10)
):
item_link = f"{self._site_proto}://{self._site_url}{row.url}"
items.append(
PyRSS2Gen.RSSItem(
title=f"{self._site_proto}://{self._site_url}{row.url} - {row.author_name}",
link=item_link,
description=markdownizer.convert(row.content),
guid=PyRSS2Gen.Guid(f"{item_link}{row.id}"),
pubDate=row.published,
)
)
rss_title = f"Commentaires du site {self._site_name}"
rss = PyRSS2Gen.RSS2(
title=rss_title,
link=f"{self._site_proto}://{self._site_url}",
description=rss_title,
lastBuildDate=datetime.now(),
items=items,
)
# TODO technical debt: replace pyRss2Gen
# TODO validate feed (https://validator.w3.org/feed/check.cgi)
# pylint: disable=consider-using-with
rss.write_xml(open(self._rss_file, "w", encoding="utf-8"), encoding="utf-8")

@ -25,7 +25,6 @@ def client():
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
database.setup(":memory:") database.setup(":memory:")
init_test_db() init_test_db()
app.config.update(SITE_TOKEN="ETC")
logger.info(f"start interface {api}") logger.info(f"start interface {api}")
return app.test_client() return app.test_client()

@ -1,41 +1,34 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import unittest import pytest
from stacosys.conf.config import Config, ConfigParameter from stacosys.service import config
from stacosys.service.configuration import ConfigParameter
EXPECTED_DB_SQLITE_FILE = "db.sqlite" EXPECTED_DB_SQLITE_FILE = "db.sqlite"
EXPECTED_HTTP_PORT = 8080 EXPECTED_HTTP_PORT = 8080
EXPECTED_LANG = "fr" EXPECTED_LANG = "fr"
class ConfigTestCase(unittest.TestCase): @pytest.fixture
def setUp(self): def init_config():
self.conf = Config() config.put(ConfigParameter.DB_SQLITE_FILE, EXPECTED_DB_SQLITE_FILE)
self.conf.put(ConfigParameter.DB_SQLITE_FILE, EXPECTED_DB_SQLITE_FILE) config.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
self.conf.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
def test_exists(init_config):
def test_exists(self): assert config.exists(ConfigParameter.DB_SQLITE_FILE)
self.assertTrue(self.conf.exists(ConfigParameter.DB_SQLITE_FILE))
def test_get(init_config):
def test_get(self): assert config.get(ConfigParameter.DB_SQLITE_FILE) == EXPECTED_DB_SQLITE_FILE
self.assertEqual( assert config.get(ConfigParameter.HTTP_HOST) == ""
self.conf.get(ConfigParameter.DB_SQLITE_FILE), EXPECTED_DB_SQLITE_FILE assert config.get(ConfigParameter.HTTP_PORT) == str(EXPECTED_HTTP_PORT)
) assert config.get_int(ConfigParameter.HTTP_PORT) == EXPECTED_HTTP_PORT
self.assertIsNone(self.conf.get(ConfigParameter.HTTP_HOST)) with pytest.raises(AssertionError):
self.assertEqual( config.get_bool(ConfigParameter.DB_SQLITE_FILE)
self.conf.get(ConfigParameter.HTTP_PORT), str(EXPECTED_HTTP_PORT)
) def test_put(init_config):
self.assertEqual(self.conf.get_int(ConfigParameter.HTTP_PORT), 8080) assert not config.exists(ConfigParameter.LANG)
try: config.put(ConfigParameter.LANG, EXPECTED_LANG)
self.conf.get_bool(ConfigParameter.DB_SQLITE_FILE) assert config.exists(ConfigParameter.LANG)
self.assertTrue(False) assert config.get(ConfigParameter.LANG) == EXPECTED_LANG
except AssertionError:
pass
def test_put(self):
self.assertFalse(self.conf.exists(ConfigParameter.LANG))
self.conf.put(ConfigParameter.LANG, EXPECTED_LANG)
self.assertTrue(self.conf.exists(ConfigParameter.LANG))
self.assertEqual(self.conf.get(ConfigParameter.LANG), EXPECTED_LANG)

@ -1,53 +1,53 @@
import unittest #!/usr/bin/python
# -*- coding: UTF-8 -*-
import pytest
from stacosys.db import dao from stacosys.db import dao
from stacosys.db import database from stacosys.db import database
@pytest.fixture
def setup_db():
database.setup(":memory:")
def test_dao_published(setup_db):
# test count published
assert 0 == dao.count_published_comments("")
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
assert 0 == dao.count_published_comments("")
dao.publish_comment(c1)
assert 1 == dao.count_published_comments("")
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
dao.publish_comment(c2)
assert 2 == dao.count_published_comments("")
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
dao.publish_comment(c3)
assert 1 == dao.count_published_comments("/post1")
assert 2 == dao.count_published_comments("/post2")
# test find published
assert 0 == len(dao.find_published_comments_by_url("/"))
assert 1 == len(dao.find_published_comments_by_url("/post1"))
assert 2 == len(dao.find_published_comments_by_url("/post2"))
dao.delete_comment(c1)
assert 0 == len(dao.find_published_comments_by_url("/post1"))
def test_dao_notified(setup_db):
# test count notified
assert 0 == len(dao.find_not_notified_comments())
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
assert 1 == len(dao.find_not_notified_comments())
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
assert 2 == len(dao.find_not_notified_comments())
dao.notify_comment(c1)
dao.notify_comment(c2)
assert 0 == len(dao.find_not_notified_comments())
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
assert 1 == len(dao.find_not_notified_comments())
dao.notify_comment(c3)
assert 0 == len(dao.find_not_notified_comments())
class DbTestCase(unittest.TestCase):
def setUp(self):
database.setup(":memory:")
def test_dao_published(self):
# test count published
self.assertEqual(0, dao.count_published_comments(""))
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
self.assertEqual(0, dao.count_published_comments(""))
dao.publish_comment(c1)
self.assertEqual(1, dao.count_published_comments(""))
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
dao.publish_comment(c2)
self.assertEqual(2, dao.count_published_comments(""))
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
dao.publish_comment(c3)
self.assertEqual(1, dao.count_published_comments("/post1"))
self.assertEqual(2, dao.count_published_comments("/post2"))
# test find published
self.assertEqual(0, len(dao.find_published_comments_by_url("/")))
self.assertEqual(1, len(dao.find_published_comments_by_url("/post1")))
self.assertEqual(2, len(dao.find_published_comments_by_url("/post2")))
dao.delete_comment(c1)
self.assertEqual(0, len(dao.find_published_comments_by_url("/post1")))
def test_dao_notified(self):
# test count notified
self.assertEqual(0, len(dao.find_not_notified_comments()))
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
self.assertEqual(1, len(dao.find_not_notified_comments()))
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
self.assertEqual(2, len(dao.find_not_notified_comments()))
dao.notify_comment(c1)
dao.notify_comment(c2)
self.assertEqual(0, len(dao.find_not_notified_comments()))
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
self.assertEqual(1, len(dao.find_not_notified_comments()))
dao.notify_comment(c3)
self.assertEqual(0, len(dao.find_not_notified_comments()))
if __name__ == "__main__":
unittest.main()

@ -13,8 +13,7 @@ from stacosys.interface import form
@pytest.fixture @pytest.fixture
def client(): def client():
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
database.setup(":memory:") database.setup(":memory:")
app.config.update(SITE_REDIRECT="/redirect")
logger.info(f"start interface {form}") logger.info(f"start interface {form}")
return app.test_client() return app.test_client()

@ -0,0 +1,11 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import pytest
from stacosys.service import mailer
def test_configure_and_check():
mailer.configure_smtp("localhost", 2525, "admin", "admin")
mailer.configure_destination("admin@mydomain.com")
with pytest.raises(ConnectionRefusedError):
mailer.check()

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from stacosys.service import rss
def test_configure():
rss.configure("comments.xml", "blog", "http", "blog.mydomain.com")

@ -1,9 +0,0 @@
[tox]
isolated_build = true
envlist = py38, py39
[testenv]
whitelist_externals = poetry
commands =
poetry install -v
poetry run pytest tests/
Loading…
Cancel
Save