From 940ea526cd211e656f076d8b5de0959ed358e29a Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 11 Oct 2021 19:23:42 +0200 Subject: [PATCH 01/65] Started on async worker for pyqt --- src/asyncworker.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/asyncworker.py diff --git a/src/asyncworker.py b/src/asyncworker.py new file mode 100644 index 00000000..c869eb73 --- /dev/null +++ b/src/asyncworker.py @@ -0,0 +1,78 @@ +import sys +import traceback + +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.uic import * + + +class WorkerSignals(QObject): + """ + Defines the signals available from a running worker thread. + + Supported signals are: + + finished + No data + + error + `tuple` (exctype, value, traceback.format_exc() ) + + result + `object` data returned from processing, anything + + progress + `int` indicating % progress + + """ + + finished = pyqtSignal() + error = pyqtSignal(tuple) + result = pyqtSignal(object) + progress = pyqtSignal(int) + + +class Worker(QRunnable): + """ + Worker thread + + Inherits from QRunnable to handler worker thread setup, signals and wrap-up. + + :param callback: The function callback to run on this worker thread. Supplied args and + kwargs will be passed through to the runner. + :type callback: function + :param args: Arguments to pass to the callback function + :param kwargs: Keywords to pass to the callback function + + """ + + def __init__(self, fn, *args, **kwargs): + super(Worker, self).__init__() + + # Store constructor arguments (re-used for processing) + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + + # Add the callback to our kwargs + # self.kwargs["progress_callback"] = self.signals.progress + + @pyqtSlot() + def run(self): + """ + Initialise the runner function with passed args, kwargs. + """ + + # Retrieve args/kwargs here; and fire processing using them + try: + result = self.fn(*self.args, **self.kwargs) + except: + traceback.print_exc() + exctype, value = sys.exc_info()[:2] + self.signals.error.emit((exctype, value, traceback.format_exc())) + else: + self.signals.result.emit(result) # Return the result of the processing + finally: + self.signals.finished.emit() # Done From c0dd406bd5433b4f3c4dd7866023980ccdd5412f Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 11 Oct 2021 19:23:52 +0200 Subject: [PATCH 02/65] Updated pyqt --- poetry.lock | 479 +++++++++++++++++++++++++++++++++++-------------- pyproject.toml | 35 ++-- 2 files changed, 367 insertions(+), 147 deletions(-) diff --git a/poetry.lock b/poetry.lock index e9f9ec66..9a897477 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,130 +1,349 @@ -[[package]] -name = "certifi" -version = "2020.12.5" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "idna" -version = "2.10" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyqt5" -version = "5.15.2" -description = "Python bindings for the Qt cross platform application toolkit" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -PyQt5-sip = ">=12.8,<13" - -[[package]] -name = "pyqt5-sip" -version = "12.8.1" -description = "The sip module support for PyQt5" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "requests" -version = "2.25.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] - -[[package]] -name = "urllib3" -version = "1.26.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[metadata] -lock-version = "1.1" -python-versions = "3.6" -content-hash = "01e9aa68475fc1a8ad3f10c9154a6396b2d72c8606941e4681b23d418cdae731" - -[metadata.files] -certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, -] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, -] -idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, -] -pyqt5 = [ - {file = "PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:894ca4ae767a8d6cf5903784b71f755073c78cb8c167eecf6e4ed6b3b055ac6a"}, - {file = "PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:29889845688a54d62820585ad5b2e0200a36b304ff3d7a555e95599f110ba4ce"}, - {file = "PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:ea24f24b7679bf393dd2e4f53fe0ce65021be18304c1ff7a226c2fc5c356d0da"}, - {file = "PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:faaecb76ec65e12673a968e7f5bc02495957e6996f0a3fa0d98895f9e4113746"}, - {file = "PyQt5-5.15.2.tar.gz", hash = "sha256:372b08dc9321d1201e4690182697c5e7ffb2e0770e6b4a45519025134b12e4fc"}, -] -pyqt5-sip = [ - {file = "PyQt5_sip-12.8.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c"}, - {file = "PyQt5_sip-12.8.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2"}, - {file = "PyQt5_sip-12.8.1-cp35-cp35m-win32.whl", hash = "sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194"}, - {file = "PyQt5_sip-12.8.1-cp35-cp35m-win_amd64.whl", hash = "sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb"}, - {file = "PyQt5_sip-12.8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115"}, - {file = "PyQt5_sip-12.8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369"}, - {file = "PyQt5_sip-12.8.1-cp36-cp36m-win32.whl", hash = "sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9"}, - {file = "PyQt5_sip-12.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c"}, - {file = "PyQt5_sip-12.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a"}, - {file = "PyQt5_sip-12.8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec"}, - {file = "PyQt5_sip-12.8.1-cp37-cp37m-win32.whl", hash = "sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9"}, - {file = "PyQt5_sip-12.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0"}, - {file = "PyQt5_sip-12.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f"}, - {file = "PyQt5_sip-12.8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0"}, - {file = "PyQt5_sip-12.8.1-cp38-cp38-win32.whl", hash = "sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6"}, - {file = "PyQt5_sip-12.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507"}, - {file = "PyQt5_sip-12.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5"}, - {file = "PyQt5_sip-12.8.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777"}, - {file = "PyQt5_sip-12.8.1-cp39-cp39-win32.whl", hash = "sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d"}, - {file = "PyQt5_sip-12.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13"}, - {file = "PyQt5_sip-12.8.1.tar.gz", hash = "sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd"}, -] -requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, -] -urllib3 = [ - {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, - {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, -] +[[package]] +name = "astroid" +version = "2.8.2" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<1.13" + +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "isort" +version = "5.8.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.6.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pylint" +version = "2.11.1" +description = "python code static checker" +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +astroid = ">=2.8.0,<2.9" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" +toml = ">=0.7.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[[package]] +name = "pyqt5" +version = "5.15.4" +description = "Python bindings for the Qt cross platform application toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +PyQt5-Qt5 = ">=5.15" +PyQt5-sip = ">=12.8,<13" + +[[package]] +name = "pyqt5-qt5" +version = "5.15.2" +description = "The subset of a Qt installation needed by PyQt5." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyqt5-sip" +version = "12.8.1" +description = "The sip module support for PyQt5" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[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]] +name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "wrapt" +version = "1.12.1" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "*" + +[metadata] +lock-version = "1.1" +python-versions = "3.6" +content-hash = "e1a04afcf8fd59df2d3006f55e92d72246f15d2730c4e2ab6eb886aaed37b687" + +[metadata.files] +astroid = [ + {file = "astroid-2.8.2-py3-none-any.whl", hash = "sha256:9eaeaf92b3e21b70bec1a262e7eb118d2e96294892a5de595c92a12adc80dfc2"}, + {file = "astroid-2.8.2.tar.gz", hash = "sha256:304e99c129794f2cfda584a12b71fde85205da950e2f330f4be09150525ae949"}, +] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +isort = [ + {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, + {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +platformdirs = [ + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, +] +pylint = [ + {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, + {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, +] +pyqt5 = [ + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:8c0848ba790a895801d5bfd171da31cad3e551dbcc4e59677a3b622de2ceca98"}, + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:883a549382fc22d29a0568f3ef20b38c8e7ab633a59498ac4eb63a3bf36d3fd3"}, + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:a88526a271e846e44779bb9ad7a738c6d3c4a9d01e15a128ecfc6dd4696393b7"}, + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:213bebd51821ed89b4d5b35bb10dbe67564228b3568f463a351a08e8b1677025"}, + {file = "PyQt5-5.15.4.tar.gz", hash = "sha256:2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be"}, +] +pyqt5-qt5 = [ + {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"}, +] +pyqt5-sip = [ + {file = "PyQt5_sip-12.8.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c"}, + {file = "PyQt5_sip-12.8.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2"}, + {file = "PyQt5_sip-12.8.1-cp35-cp35m-win32.whl", hash = "sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194"}, + {file = "PyQt5_sip-12.8.1-cp35-cp35m-win_amd64.whl", hash = "sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb"}, + {file = "PyQt5_sip-12.8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115"}, + {file = "PyQt5_sip-12.8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369"}, + {file = "PyQt5_sip-12.8.1-cp36-cp36m-win32.whl", hash = "sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9"}, + {file = "PyQt5_sip-12.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c"}, + {file = "PyQt5_sip-12.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a"}, + {file = "PyQt5_sip-12.8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec"}, + {file = "PyQt5_sip-12.8.1-cp37-cp37m-win32.whl", hash = "sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9"}, + {file = "PyQt5_sip-12.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0"}, + {file = "PyQt5_sip-12.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f"}, + {file = "PyQt5_sip-12.8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0"}, + {file = "PyQt5_sip-12.8.1-cp38-cp38-win32.whl", hash = "sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6"}, + {file = "PyQt5_sip-12.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507"}, + {file = "PyQt5_sip-12.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5"}, + {file = "PyQt5_sip-12.8.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777"}, + {file = "PyQt5_sip-12.8.1-cp39-cp39-win32.whl", hash = "sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d"}, + {file = "PyQt5_sip-12.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13"}, + {file = "PyQt5_sip-12.8.1.tar.gz", hash = "sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typed-ast = [ + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, +] +urllib3 = [ + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, +] +wrapt = [ + {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, +] diff --git a/pyproject.toml b/pyproject.toml index f403fd11..95ece5d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,18 @@ -[tool.poetry] -name = "Cocktailmaker" -version = "1.1.0" -description = "A Python and Qt based App for a Cocktail Maker on a Raspberry Pi" -authors = ["Andre Wohnsland "] -license = "MIT" - -[tool.poetry.dependencies] -python = "3.6" -PyQt5 = "^5.15.2" -requests = "^2.25.1" - -[tool.poetry.dev-dependencies] - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.poetry] +name = "Cocktailmaker" +version = "1.1.0" +description = "A Python and Qt based App for a Cocktail Maker on a Raspberry Pi" +authors = ["Andre Wohnsland "] +license = "MIT" + +[tool.poetry.dependencies] +python = "3.6" +PyQt5 = "^5.15.2" +requests = "^2.25.1" + +[tool.poetry.dev-dependencies] +pylint = "^2.11.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From 6414104673452558d363fc65ed11436714e9aa4b Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 11 Oct 2021 20:16:39 +0200 Subject: [PATCH 03/65] Removed globalvars and moved to config --- config/config_manager.py | 41 +++++++++++++++++++++++++--------------- globalvars.py | 17 ----------------- readme.md | 14 ++------------ 3 files changed, 28 insertions(+), 44 deletions(-) delete mode 100644 globalvars.py diff --git a/config/config_manager.py b/config/config_manager.py index 450a6295..5694a425 100644 --- a/config/config_manager.py +++ b/config/config_manager.py @@ -1,15 +1,26 @@ -class ConfigManager: - """Manager for all static configuration of the machine """ - - MASTERPASSWORD = "1337" - USEDPINS = [14, 15, 18, 23, 24, 25, 8, 7, 17, 27, 22, 20] - PUMP_VOLUMEFLOW = [30, 30, 25, 30, 30, 30, 25, 30, 30, 23, 30, 30] - NUMBER_BOTTLES = 10 - CLEAN_TIME = 20 - SLEEP_TIME = 0.05 - PARTYMODE = False - LOGGERNAME = "cocktaillogger" - LOGGERNAME_DEBUG = "debuglogger" - USE_MICROSERVICE = True - MICROSERVICE_BASE_URL = "http://127.0.0.1:5000" - DEVENVIRONMENT = True +class ConfigManager: + """Manager for all static configuration of the machine """ + + MASTERPASSWORD = "1337" + USEDPINS = [14, 15, 18, 23, 24, 25, 8, 7, 17, 27, 22, 20] + PUMP_VOLUMEFLOW = [30, 30, 25, 30, 30, 30, 25, 30, 30, 23, 30, 30] + NUMBER_BOTTLES = 10 + CLEAN_TIME = 20 + SLEEP_TIME = 0.05 + PARTYMODE = False + LOGGERNAME = "cocktaillogger" + LOGGERNAME_DEBUG = "debuglogger" + USE_MICROSERVICE = False + MICROSERVICE_BASE_URL = "http://127.0.0.1:5000" + DEVENVIRONMENT = True + + +class Shared: + def __init__(self): + self.cocktail_started = False + self.make_cocktail = True + self.supress_error = False + self.old_ingredient = [] + + +shared = Shared() diff --git a/globalvars.py b/globalvars.py deleted file mode 100644 index bcaec88f..00000000 --- a/globalvars.py +++ /dev/null @@ -1,17 +0,0 @@ -""" Initialise all neccecary global Values, which are passed -over several Modules in the Python Script. -Also assigns the values to them. -""" - - -def initialize(): - """ The Initialise Function for the global variables. """ - global cocktail_started - global make_cocktail - global SUPPRESS_ERROR - global old_ingredient - - cocktail_started = False - make_cocktail = True - SUPPRESS_ERROR = False - old_ingredient = [] diff --git a/readme.md b/readme.md index 9fe11d27..07bc7be5 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,6 @@ - [Development](#development) - [Program Schema](#program-schema) - [Pull Requests and Issues](#pull-requests-and-issues) - - [Caveats from Past Code](#caveats-from-past-code) - [Side Notes](#side-notes) - [ToDos](#todos) @@ -148,6 +147,8 @@ These values are stored under the `config/config_manager.py` file. Depending on - `MICROSERVICE_BASE_URL` base url for microservice (if default docker it is at http://127.0.0.1:5000) - `DEVENVIRONMENT` boolean flag to enable some development features +In addition, there is a `Shared` config class, with dynamic values. The only thing you may want to change is `supress_error` to true, this will activate a wrapper function catching and logging errors of the wrapped function. In production this will effectivly prevent the app from crashing due to errors (bugs) and log them, but setting it to `True` is at own risk. + Depending on your preferred use, these values can differ. Then just run `runme.py`. Setting up the machine is quite easy as well. Just go to the **Belegung** Tab and select via the dropdown boxes your assigned ingredients. In addition, you can define ingredients which are also there, but are not connected to the machine (under _Zutaten/Ingredients > verfügbar/available_). You can define ingredients in recipes (at _selbst hinzufügen / add your own_) which should be later added via hand (for example sticky ingredients which would not be optimal for your pump, or only very rarely used ones in cocktails). @@ -193,17 +194,6 @@ In the following diagram, the schema and Classes / Containers are displayed in a If you want to support this project, feel free to fork it and create your own pull request. If you run into any issues, feel free to open a ticket / issue. -## Caveats from Past Code - -There are currently still some caveats from the previous code, contained in the `globalvars.py`. These are: - -- `SUPPRESS_ERROR` serves to control the decorators. I recommend setting this value to `False` while developing and to `True` while using the machine. If it's set to `True`, the additional lines from the decorator will be carried out, otherwise the decorator will just execute the function without extra steps. The decorators can suppress the Exception Error and log it instead `(logerror)`. This is quite handy when you want to run the machine without any restarts, but also keep track if anything goes wrong. -- `cocktail_started` Boolean flag to ensure only one cocktail is run -- `make_cocktail` Boolean flag to interrupt the cocktail procedure over the Ui via a button -- `old_ingredient` Memorisation of the last order of ingredients before a change of the comboboxes - -They will be moved into the main logic classes in the future. - # Side Notes As you probably noticed, the interface is in German since this is the native language of all my friends (which are the users of the machine). I am planning to translate all text to English at some point, and give the possibility to choose between both languages, but currently there is no planned date for that. From f9a7fdca873b151c6310a7f88cb7bbd7e487d2bf Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 11 Oct 2021 20:23:58 +0200 Subject: [PATCH 04/65] Added autopep8 --- poetry.lock | 30 +++- pyproject.toml | 1 + runme.py | 28 ++-- src/bottles.py | 287 ++++++++++++++++++++------------------- src/error_suppression.py | 46 +++---- src/maker.py | 18 +-- src/rpi_controller.py | 186 ++++++++++++------------- 7 files changed, 311 insertions(+), 285 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9a897477..e9968a8c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,18 @@ typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpyth typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<1.13" +[[package]] +name = "autopep8" +version = "1.5.7" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycodestyle = ">=2.7.0" +toml = "*" + [[package]] name = "certifi" version = "2020.12.5" @@ -85,6 +97,14 @@ python-versions = ">=3.6" docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pylint" version = "2.11.1" @@ -196,13 +216,17 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "3.6" -content-hash = "e1a04afcf8fd59df2d3006f55e92d72246f15d2730c4e2ab6eb886aaed37b687" +content-hash = "ac9eb757ef3046392b4772ca518f7f5ae20ffa03d0ae503a48edb35cfd1ebf5a" [metadata.files] astroid = [ {file = "astroid-2.8.2-py3-none-any.whl", hash = "sha256:9eaeaf92b3e21b70bec1a262e7eb118d2e96294892a5de595c92a12adc80dfc2"}, {file = "astroid-2.8.2.tar.gz", hash = "sha256:304e99c129794f2cfda584a12b71fde85205da950e2f330f4be09150525ae949"}, ] +autopep8 = [ + {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"}, + {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"}, +] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, @@ -255,6 +279,10 @@ platformdirs = [ {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] pylint = [ {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, diff --git a/pyproject.toml b/pyproject.toml index 95ece5d1..75fc13e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ requests = "^2.25.1" [tool.poetry.dev-dependencies] pylint = "^2.11.1" +autopep8 = "^1.5.7" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/runme.py b/runme.py index 4b53330b..38eb4d62 100644 --- a/runme.py +++ b/runme.py @@ -1,16 +1,12 @@ -import sys -from PyQt5.QtWidgets import QApplication - -import src_ui.setup_mainwindow as setupui -import globalvars - - -globalvars.initialize() - - -if __name__ == "__main__": - app = QApplication(sys.argv) - w = setupui.MainScreen() - w.showFullScreen() - w.setFixedSize(800, 480) - sys.exit(app.exec_()) +import sys +from PyQt5.QtWidgets import QApplication + +import src_ui.setup_mainwindow as setupui + + +if __name__ == "__main__": + app = QApplication(sys.argv) + w = setupui.MainScreen() + w.showFullScreen() + w.setFixedSize(800, 480) + sys.exit(app.exec_()) diff --git a/src/bottles.py b/src/bottles.py index 6a5018f6..3ece146c 100644 --- a/src/bottles.py +++ b/src/bottles.py @@ -1,143 +1,144 @@ -# -*- coding: utf-8 -*- -""" Module with all nececcary functions for the bottles Tab. -This includes all functions for the Lists, DB and Buttos/Dropdowns. -""" - -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * - -import globalvars -from src.error_suppression import logerror - -from src.display_handler import DisplayHandler -from src.database_commander import DatabaseCommander -from src.display_controller import DisplayController -from src.rpi_controller import RpiController -from src.logger_handler import LoggerHandler -from src.supporter import ( - generate_CBB_names, - generate_LBelegung_names, - generate_PBneu_names, - generate_ProBBelegung_names, -) - - -DP_HANDLER = DisplayHandler() -DB_COMMANDER = DatabaseCommander() -DP_CONTROLLER = DisplayController() -RPI_CONTROLLER = RpiController() -LOG_HANDLER = LoggerHandler("bottles_module", "production_logs") - - -def customlevels(w): - """ Opens the additional window to change the volume levels of the bottles. """ - bot_names = [] - vol_values = [] - w.bottleswindow(bot_names, vol_values) - - -def get_bottle_ingredients(): - """ At the start of the Programm, get all the ingredients from the DB. """ - bottles = DB_COMMANDER.get_ingredients_at_bottles() - globalvars.old_ingredient.extend(bottles) - - -def refresh_bottle_cb(w): - """ Adds or remove items to the bottle comboboxes depending on the changed value""" - # Creating a list of the new and old bottles used - combobox_bottles = generate_CBB_names(w) - old_order = globalvars.old_ingredient - new_order = DP_CONTROLLER.get_current_combobox_items(combobox_bottles) - - new_blist = list(set(new_order) - set(old_order)) - old_blist = list(set(old_order) - set(new_order)) - new_bottle = new_blist[0] if new_blist else "" - old_bottle = old_blist[0] if old_blist else "" - - DP_HANDLER.adjust_bottle_comboboxes(combobox_bottles, old_bottle, new_bottle) - - register_bottles(w) - globalvars.old_ingredient = new_order - - -@logerror -def calculate_combobox_bottles(w): - """ Fills each bottle combobox with the possible remaining options - """ - combobox_bottles = generate_CBB_names(w) - used_ingredients = globalvars.old_ingredient - possible_ingredients = DB_COMMANDER.get_ingredient_names_machine() - - shown_ingredients = [] - for row, _ in enumerate(used_ingredients): - shown_ingredients.append(sorted(set(possible_ingredients) - set([x for i, x in enumerate(used_ingredients) if i != row]))) - - DP_HANDLER.fill_multiple_combobox_individually(combobox_bottles, shown_ingredients, True) - - -@logerror -def register_bottles(w): - """ Insert the selected Bottleorder into the DB. """ - # this import is neccecary on module level, otherwise there would be a circular import - from src.maker import refresh_recipe_maker_view - - # Checks where are entries and appends them to a list - combobox_bottles = generate_CBB_names(w) - ingredient_names = DP_CONTROLLER.get_current_combobox_items(combobox_bottles) - DB_COMMANDER.set_bottleorder(ingredient_names) - - refresh_bottle_information(w) - w.LWMaker.clear() - refresh_recipe_maker_view(w) - set_fill_level_bars(w) - - -@logerror -def read_in_bottles(w): - """ Reads the Bottleorder into the BottleTab. """ - combobox_bottles = generate_CBB_names(w) - ingredient_names = DB_COMMANDER.get_ingredients_at_bottles() - DP_HANDLER.set_multiple_combobox_items(combobox_bottles, ingredient_names) - - -@logerror -def refresh_bottle_information(w): - """ Loads or updates the Labels of the Bottles (Volumelevel). """ - labels = generate_LBelegung_names(w) - label_names = DB_COMMANDER.get_ingredients_at_bottles() - label_names = [f" {x}:" if x != "" else " - " for x in label_names] - DP_HANDLER.fill_multiple_lineedit(labels, label_names) - - -@logerror -def renew_checked_bottles(w): - """ Renews all the Bottles which are checked as new. """ - pushbutton_new_list = generate_PBneu_names(w) - renew_bottle = DP_CONTROLLER.get_toggle_status(pushbutton_new_list) - DB_COMMANDER.set_bottle_volumelevel_to_max(renew_bottle) - DP_HANDLER.untoggle_buttons(pushbutton_new_list) - set_fill_level_bars(w) - DP_HANDLER.standard_box("Alle Flaschen angewendet!") - - -@logerror -def set_fill_level_bars(w): - """ Gets the proportion of actual and maximal volume of each connected bottle and asigns it""" - progressbars = generate_ProBBelegung_names(w) - fill_levels = DB_COMMANDER.get_bottle_fill_levels() - DP_HANDLER.set_progress_bar_values(progressbars, fill_levels) - - -@logerror -def clean_machine(w): - """ Activate all Pumps for 20 s to clean them. Needs the Password. Logs the Event. """ - if not DP_CONTROLLER.check_bottles_password(w): - DP_HANDLER.standard_box("Falsches Passwort!!!!") - return - - DP_HANDLER.standard_box("Achtung!: Maschine wird gereinigt, genug Wasser bereitstellen! Ok zum Fortfahren.") - LOG_HANDLER.log_header("INFO", "Cleaning the Pumps") - RPI_CONTROLLER.clean_pumps() - DP_HANDLER.standard_box("Fertig!!!") +# -*- coding: utf-8 -*- +""" Module with all nececcary functions for the bottles Tab. +This includes all functions for the Lists, DB and Buttos/Dropdowns. +""" + +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.uic import * + +from config.config_manager import shared +from src.error_suppression import logerror + +from src.display_handler import DisplayHandler +from src.database_commander import DatabaseCommander +from src.display_controller import DisplayController +from src.rpi_controller import RpiController +from src.logger_handler import LoggerHandler +from src.supporter import ( + generate_CBB_names, + generate_LBelegung_names, + generate_PBneu_names, + generate_ProBBelegung_names, +) + + +DP_HANDLER = DisplayHandler() +DB_COMMANDER = DatabaseCommander() +DP_CONTROLLER = DisplayController() +RPI_CONTROLLER = RpiController() +LOG_HANDLER = LoggerHandler("bottles_module", "production_logs") + + +def customlevels(w): + """ Opens the additional window to change the volume levels of the bottles. """ + bot_names = [] + vol_values = [] + w.bottleswindow(bot_names, vol_values) + + +def get_bottle_ingredients(): + """ At the start of the Programm, get all the ingredients from the DB. """ + bottles = DB_COMMANDER.get_ingredients_at_bottles() + shared.old_ingredient = bottles + + +def refresh_bottle_cb(w): + """ Adds or remove items to the bottle comboboxes depending on the changed value""" + # Creating a list of the new and old bottles used + combobox_bottles = generate_CBB_names(w) + old_order = shared.old_ingredient + new_order = DP_CONTROLLER.get_current_combobox_items(combobox_bottles) + + new_blist = list(set(new_order) - set(old_order)) + old_blist = list(set(old_order) - set(new_order)) + new_bottle = new_blist[0] if new_blist else "" + old_bottle = old_blist[0] if old_blist else "" + + DP_HANDLER.adjust_bottle_comboboxes(combobox_bottles, old_bottle, new_bottle) + + register_bottles(w) + shared.old_ingredient = new_order + + +@logerror +def calculate_combobox_bottles(w): + """ Fills each bottle combobox with the possible remaining options + """ + combobox_bottles = generate_CBB_names(w) + used_ingredients = shared.old_ingredient + possible_ingredients = DB_COMMANDER.get_ingredient_names_machine() + + shown_ingredients = [] + for row, _ in enumerate(used_ingredients): + shown_ingredients.append(sorted(set(possible_ingredients) - + {x for i, x in enumerate(used_ingredients) if i != row})) + + DP_HANDLER.fill_multiple_combobox_individually(combobox_bottles, shown_ingredients, True) + + +@logerror +def register_bottles(w): + """ Insert the selected Bottleorder into the DB. """ + # this import is neccecary on module level, otherwise there would be a circular import + from src.maker import refresh_recipe_maker_view + + # Checks where are entries and appends them to a list + combobox_bottles = generate_CBB_names(w) + ingredient_names = DP_CONTROLLER.get_current_combobox_items(combobox_bottles) + DB_COMMANDER.set_bottleorder(ingredient_names) + + refresh_bottle_information(w) + w.LWMaker.clear() + refresh_recipe_maker_view(w) + set_fill_level_bars(w) + + +@logerror +def read_in_bottles(w): + """ Reads the Bottleorder into the BottleTab. """ + combobox_bottles = generate_CBB_names(w) + ingredient_names = DB_COMMANDER.get_ingredients_at_bottles() + DP_HANDLER.set_multiple_combobox_items(combobox_bottles, ingredient_names) + + +@logerror +def refresh_bottle_information(w): + """ Loads or updates the Labels of the Bottles (Volumelevel). """ + labels = generate_LBelegung_names(w) + label_names = DB_COMMANDER.get_ingredients_at_bottles() + label_names = [f" {x}:" if x != "" else " - " for x in label_names] + DP_HANDLER.fill_multiple_lineedit(labels, label_names) + + +@logerror +def renew_checked_bottles(w): + """ Renews all the Bottles which are checked as new. """ + pushbutton_new_list = generate_PBneu_names(w) + renew_bottle = DP_CONTROLLER.get_toggle_status(pushbutton_new_list) + DB_COMMANDER.set_bottle_volumelevel_to_max(renew_bottle) + DP_HANDLER.untoggle_buttons(pushbutton_new_list) + set_fill_level_bars(w) + DP_HANDLER.standard_box("Alle Flaschen angewendet!") + + +@logerror +def set_fill_level_bars(w): + """ Gets the proportion of actual and maximal volume of each connected bottle and asigns it""" + progressbars = generate_ProBBelegung_names(w) + fill_levels = DB_COMMANDER.get_bottle_fill_levels() + DP_HANDLER.set_progress_bar_values(progressbars, fill_levels) + + +@logerror +def clean_machine(w): + """ Activate all Pumps for 20 s to clean them. Needs the Password. Logs the Event. """ + if not DP_CONTROLLER.check_bottles_password(w): + DP_HANDLER.standard_box("Falsches Passwort!!!!") + return + + DP_HANDLER.standard_box("Achtung!: Maschine wird gereinigt, genug Wasser bereitstellen! Ok zum Fortfahren.") + LOG_HANDLER.log_header("INFO", "Cleaning the Pumps") + RPI_CONTROLLER.clean_pumps() + DP_HANDLER.standard_box("Fertig!!!") diff --git a/src/error_suppression.py b/src/error_suppression.py index 02da8870..296f2d70 100644 --- a/src/error_suppression.py +++ b/src/error_suppression.py @@ -1,23 +1,23 @@ -""" Special configs for the Logger """ -import logging -from functools import wraps - -import globalvars - - -def logerror(func): - """ Logs every time an error occours """ - - @wraps(func) - def wrapper(*args, **kwargs): - if globalvars.SUPPRESS_ERROR: - logger = logging.getLogger("debuglog") - try: - func(*args, **kwargs) - except Exception: - logger.exception("The function %s could not be fully excecuted!", func.__name__) - print("The function {} could not be fully excecuted!".format(func.__name__)) - else: - func(*args, **kwargs) - - return wrapper +""" Special configs for the Logger """ +import logging +from functools import wraps + +from config.config_manager import shared + + +def logerror(func): + """ Logs every time an error occours """ + + @wraps(func) + def wrapper(*args, **kwargs): + if shared.supress_error: + logger = logging.getLogger("debuglog") + try: + func(*args, **kwargs) + except Exception: + logger.exception("The function %s could not be fully excecuted!", func.__name__) + print("The function {} could not be fully excecuted!".format(func.__name__)) + else: + func(*args, **kwargs) + + return wrapper diff --git a/src/maker.py b/src/maker.py index bb9ac315..93335be8 100644 --- a/src/maker.py +++ b/src/maker.py @@ -18,7 +18,7 @@ from src.logger_handler import LoggerHandler from src.service_handler import ServiceHandler -import globalvars +from config.config_manager import shared DB_COMMANDER = DatabaseCommander() @@ -124,7 +124,7 @@ def enough_ingredient(level, needed_volume): """Checks if the needed volume is there Accepts if there is at least 80% of needed volume to be more efficient with the remainder volume in the bottle""" - if needed_volume*0.8 > level: + if needed_volume * 0.8 > level: return False return True @@ -132,7 +132,7 @@ def enough_ingredient(level, needed_volume): def generate_maker_log_entry(cocktail_volume, cocktail_name, taken_time, max_time): """Enters a log entry for the made cocktail""" mengenstring = f"{cocktail_volume} ml" - if globalvars.make_cocktail == False: + if not shared.make_cocktail: pumped_volume = round(cocktail_volume * (taken_time) / max_time) abbruchstring = f" - Rezept wurde bei {round(taken_time, 1)} s abgebrochen - {pumped_volume} ml" else: @@ -142,7 +142,7 @@ def generate_maker_log_entry(cocktail_volume, cocktail_name, taken_time, max_tim def prepare_cocktail(w): """ Prepares a Cocktail, if not already another one is in production and enough ingredients are available""" - if globalvars.cocktail_started: + if shared.cocktail_started: return cocktailname, cocktail_volume, alcohol_faktor = DP_CONTROLLER.get_cocktail_data(w) if not cocktailname: @@ -157,8 +157,8 @@ def prepare_cocktail(w): w.tabWidget.setCurrentIndex(3) return - globalvars.cocktail_started = True - globalvars.make_cocktail = True + shared.cocktail_started = True + shared.make_cocktail = True consumption, taken_time, max_time = RPI_CONTROLLER.make_cocktail(w, ingredient_bottles, ingredient_volumes) DB_COMMANDER.set_recipe_counter(cocktailname) generate_maker_log_entry(cocktail_volume, cocktailname, taken_time, max_time) @@ -166,7 +166,7 @@ def prepare_cocktail(w): SERVICE_HANDLER.post_cocktail_to_hook(cocktailname, cocktail_volume) - if globalvars.make_cocktail: + if shared.make_cocktail: DB_COMMANDER.set_multiple_ingredient_consumption([x[0] for x in update_data], [x[1] for x in update_data]) DP_HANDLER.standard_box( f"Der Cocktail ist fertig! Bitte kurz warten, falls noch etwas nachtropft.{comment}") @@ -177,12 +177,12 @@ def prepare_cocktail(w): set_fill_level_bars(w) reset_alcohollevel(w) - globalvars.cocktail_started = False + shared.cocktail_started = False def interrupt_cocktail(): """ Interrupts the cocktail preparation. """ - globalvars.make_cocktail = False + shared.make_cocktail = False print("Rezept wird abgebrochen!") diff --git a/src/rpi_controller.py b/src/rpi_controller.py index c7ce5031..cc5050c9 100644 --- a/src/rpi_controller.py +++ b/src/rpi_controller.py @@ -1,93 +1,93 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * -import time - -import globalvars -from config.config_manager import ConfigManager - - -class RpiController(ConfigManager): - """Controler Class for all RPi related GPIO routines """ - - def __init__(self): - try: - import RPi.GPIO as GPIO - - GPIO.setmode(GPIO.BCM) - self.devenvironment = False - except ModuleNotFoundError: - self.devenvironment = True - - def clean_pumps(self): - active_pins = self.USEDPINS[: self.NUMBER_BOTTLES] - self.activate_pinlist(active_pins) - t_cleaned = 0 - while t_cleaned < self.CLEAN_TIME: - self.clean_print(t_cleaned) - t_cleaned += self.SLEEP_TIME - t_cleaned = round(t_cleaned, 2) - time.sleep(self.SLEEP_TIME) - qApp.processEvents() - self.close_pinlist(active_pins) - - def make_cocktail(self, w, bottle_list, volume_list, labelchange=""): - w.progressionqwindow(labelchange) - already_closed_pins = set() - indexes = [x - 1 for x in bottle_list] - pins = [self.USEDPINS[i] for i in indexes] - volume_flows = [self.PUMP_VOLUMEFLOW[i] for i in indexes] - pin_times = [round(volume / flow, 1) for volume, flow in zip(volume_list, volume_flows)] - max_time = max(pin_times) - current_time = 0 - consumption = [0] * len(indexes) - self.activate_pinlist(pins) - - print("---- Starting Cocktail ----") - while current_time < max_time and globalvars.make_cocktail: - for element, (pin, pin_time, volume_flow) in enumerate(zip(pins, pin_times, volume_flows)): - if pin_time > current_time: - consumption[element] += volume_flow * self.SLEEP_TIME - elif pin not in already_closed_pins: - self.close_pin(pin, current_time) - already_closed_pins.add(pin) - - self.consumption_print(consumption, current_time, max_time) - current_time += self.SLEEP_TIME - current_time = round(current_time, 2) - time.sleep(self.SLEEP_TIME) - w.prow_change(current_time / max_time * 100) - qApp.processEvents() - - print("---- Done ----") - self.close_pinlist(pins) - w.prow_close() - return [round(x) for x in consumption], current_time, max_time - - def close_pin(self, pin, current_time): - if not self.devenvironment: - GPIO.output(pin, 1) - print(f"{current_time}s: Pin number <{pin}> is closed") - - def activate_pinlist(self, pinlist): - print(f"Opening Pins: {pinlist}") - if not self.devenvironment: - for pin in pinlist: - GPIO.setup(pin, 0) - GPIO.output(pin, 0) - - def close_pinlist(self, pinlist): - print(f"Closing Pins: {pinlist}") - if not self.devenvironment: - for pin in pinlist: - GPIO.output(pin, 1) - - def consumption_print(self, consumption, current_time, max_time, interval=1): - if current_time % interval == 0: - print( - f"Making Cocktail, {current_time}/{max_time} s:\tThe consumption is currently {[round(x) for x in consumption]}") - - def clean_print(self, t_cleaned, interval=2): - if t_cleaned % interval == 0: - print(f"Cleaning, {t_cleaned}/{self.CLEAN_TIME} s\t{'.' * int(t_cleaned)}") +import time +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.uic import * + +from config.config_manager import shared +from config.config_manager import ConfigManager + + +class RpiController(ConfigManager): + """Controler Class for all RPi related GPIO routines """ + + def __init__(self): + try: + import RPi.GPIO as GPIO + + GPIO.setmode(GPIO.BCM) + self.devenvironment = False + except ModuleNotFoundError: + self.devenvironment = True + + def clean_pumps(self): + active_pins = self.USEDPINS[: self.NUMBER_BOTTLES] + self.activate_pinlist(active_pins) + t_cleaned = 0 + while t_cleaned < self.CLEAN_TIME: + self.clean_print(t_cleaned) + t_cleaned += self.SLEEP_TIME + t_cleaned = round(t_cleaned, 2) + time.sleep(self.SLEEP_TIME) + qApp.processEvents() + self.close_pinlist(active_pins) + + def make_cocktail(self, w, bottle_list, volume_list, labelchange=""): + w.progressionqwindow(labelchange) + already_closed_pins = set() + indexes = [x - 1 for x in bottle_list] + pins = [self.USEDPINS[i] for i in indexes] + volume_flows = [self.PUMP_VOLUMEFLOW[i] for i in indexes] + pin_times = [round(volume / flow, 1) for volume, flow in zip(volume_list, volume_flows)] + max_time = max(pin_times) + current_time = 0 + consumption = [0] * len(indexes) + self.activate_pinlist(pins) + + print("---- Starting Cocktail ----") + while current_time < max_time and shared.make_cocktail: + for element, (pin, pin_time, volume_flow) in enumerate(zip(pins, pin_times, volume_flows)): + if pin_time > current_time: + consumption[element] += volume_flow * self.SLEEP_TIME + elif pin not in already_closed_pins: + self.close_pin(pin, current_time) + already_closed_pins.add(pin) + + self.consumption_print(consumption, current_time, max_time) + current_time += self.SLEEP_TIME + current_time = round(current_time, 2) + time.sleep(self.SLEEP_TIME) + w.prow_change(current_time / max_time * 100) + qApp.processEvents() + + print("---- Done ----") + self.close_pinlist(pins) + w.prow_close() + return [round(x) for x in consumption], current_time, max_time + + def close_pin(self, pin, current_time): + if not self.devenvironment: + GPIO.output(pin, 1) + print(f"{current_time}s: Pin number <{pin}> is closed") + + def activate_pinlist(self, pinlist): + print(f"Opening Pins: {pinlist}") + if not self.devenvironment: + for pin in pinlist: + GPIO.setup(pin, 0) + GPIO.output(pin, 0) + + def close_pinlist(self, pinlist): + print(f"Closing Pins: {pinlist}") + if not self.devenvironment: + for pin in pinlist: + GPIO.output(pin, 1) + + def consumption_print(self, consumption, current_time, max_time, interval=1): + if current_time % interval == 0: + print( + f"Making Cocktail, {current_time}/{max_time} s:\tThe consumption is currently {[round(x) for x in consumption]}") + + def clean_print(self, t_cleaned, interval=2): + if t_cleaned % interval == 0: + print(f"Cleaning, {t_cleaned}/{self.CLEAN_TIME} s\t{'.' * int(t_cleaned)}") From 588baa148223f3bd3d8841b8aa4ab5e591dba3df Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 11 Oct 2021 20:25:03 +0200 Subject: [PATCH 05/65] Fixed a bug occuring when there multiple empty boxes --- src/display_handler.py | 394 ++++++++++++++++++++--------------------- 1 file changed, 197 insertions(+), 197 deletions(-) diff --git a/src/display_handler.py b/src/display_handler.py index 1f528d54..d7ff9ede 100644 --- a/src/display_handler.py +++ b/src/display_handler.py @@ -1,197 +1,197 @@ -import sys -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * - -from src.supporter import generate_maker_ingredients_fields, generate_maker_volume_fields, generate_CBR_names, generate_lineedit_recipes - - -class DisplayHandler: - """Handler Class to set/remove elements from to UI """ - - def __init__(self): - pass - - def standard_box(self, textstring): - """ The default messagebox for the Maker. Uses a QMessageBox with OK-Button """ - messagebox = QMessageBox() - messagebox.setStandardButtons(QMessageBox.Ok) - buttonok = messagebox.button(QMessageBox.Ok) - buttonok.setText(" OK ") - fillstring = "-" * 70 - messagebox.setText(f"{fillstring}\n{textstring}\n{fillstring}") - messagebox.setStyleSheet( - "QMessageBox QPushButton{background-color: rgb(0, 123, 255); color: rgb(0, 0, 0); font-size: 30pt;} QMessageBox{background-color: rgb(10, 10, 10); font-size: 16pt;} QMessageBox QLabel{color: rgb(0, 123, 255);}" - ) - messagebox.showFullScreen() - messagebox.exec_() - - # LineEdit - def clean_multiple_lineedit(self, lineedit_list): - for lineedit in lineedit_list: - lineedit.clear() - - def fill_multiple_lineedit(self, lineedit_list, text_list): - for lineedit, text in zip(lineedit_list, text_list): - lineedit.setText(str(text)) - - # Combobox - def fill_single_combobox(self, combobox, itemlist, clear_first=False, sort_items=True, first_empty=True): - if clear_first: - combobox.clear() - if combobox.count() == 0 and first_empty: - combobox.addItem("") - combobox.addItems(itemlist) - if sort_items: - combobox.model().sort(0) - - def fill_multiple_combobox(self, combobox_list, itemlist, clear_first=False, sort_items=True, first_empty=True): - for combobox in combobox_list: - self.fill_single_combobox(combobox, itemlist, clear_first, sort_items, first_empty) - - def fill_multiple_combobox_individually(self, combobox_list, list_of_itemlist, clear_first=False, sort_items=True, first_empty=True): - for combobox, itemlist in zip(combobox_list, list_of_itemlist): - self.fill_single_combobox(combobox, itemlist, clear_first, sort_items, first_empty) - - def delete_single_combobox_item(self, combobox, item): - index = combobox.findText(item, Qt.MatchFixedString) - if index >= 0: - combobox.removeItem(index) - - def delete_multiple_combobox_item(self, combobox, itemlist): - for item in itemlist: - self.delete_single_combobox_item(combobox, item) - - def delete_item_in_multiple_combobox(self, combobox_list, item): - for combobox in combobox_list: - self.delete_single_combobox_item(combobox, item) - - def sort_multiple_combobox(self, combobox_list): - for combobox in combobox_list: - combobox.sort() - - def set_multiple_combobox_to_top_item(self, combobox_list): - for combobox in combobox_list: - combobox.setCurrentIndex(0) - - def set_multiple_combobox_items(self, combobox_list, items_to_set): - for combobox, item in zip(combobox_list, items_to_set): - self.set_combobox_item(combobox, item) - - def set_combobox_item(self, combobox, item): - index = combobox.findText(item, Qt.MatchFixedString) - combobox.setCurrentIndex(index) - - def adjust_bottle_comboboxes(self, combobox_list, old_item, new_item): - for combobox in combobox_list: - if (old_item != "") and (new_item != combobox.currentText()): - combobox.addItem(old_item) - if (new_item != "") and (new_item != combobox.currentText()): - self.delete_single_combobox_item(combobox, new_item) - combobox.model().sort(0) - - def rename_single_combobox(self, combobox, old_item, new_item): - index = combobox.findText(old_item, Qt.MatchFixedString) - if index >= 0: - combobox.setItemText(index, new_item) - combobox.model().sort(0) - - def rename_multiple_combobox(self, combobox_list, old_item, new_item): - for combobox in combobox_list: - self.rename_single_combobox(combobox, old_item, new_item) - - # buttons / togglebuttons - def untoggle_buttons(self, button_list): - for button in button_list: - button.setChecked(False) - - # progress bars - def set_progress_bar_values(self, progress_bar_list, value_list): - for progress_bar, value in zip(progress_bar_list, value_list): - progress_bar.setValue(value) - - # listwidget - def unselect_list_widget_items(self, list_widget): - for i in range(list_widget.count()): - list_widget.item(i).setSelected(False) - - def delete_list_widget_item(self, list_widget, item): - index_to_delete = list_widget.findItems(item, Qt.MatchExactly) - if len(index_to_delete) > 0: - for index in index_to_delete: - list_widget.takeItem(list_widget.row(index)) - - def fill_list_widget(self, list_widget, item_list): - for item in item_list: - list_widget.addItem(item) - - # checkboxes - def set_checkbox_value(self, checkbox, value): - if value: - checkbox.setChecked(True) - else: - checkbox.setChecked(False) - - # label - def set_alcohol_level(self, w, value): - w.LAlkoholgehalt.setText(f"Alkohol: {value:.0f}%") - - # others - def fill_recipe_data_maker(self, w, display_data, total_volume, cocktailname): - w.LAlkoholname.setText(cocktailname) - w.LMenge.setText(f"Menge: {total_volume} ml") - fields_ingredient = generate_maker_ingredients_fields(w)[: len(display_data)] - fields_volume = generate_maker_volume_fields(w)[: len(display_data)] - for field_ingredient, field_volume, (ingredient_name, volume) in zip(fields_ingredient, fields_volume, display_data): - field_ingredient.setText(f"{ingredient_name} ") - if volume != "": - field_volume.setText(f" {volume} ml") - if ingredient_name == "Selbst hinzufügen:": - field_ingredient.setStyleSheet("color: rgb(170, 170, 170)") - - def clear_recipe_data_maker(self, w): - w.LAlkoholgehalt.setText("") - w.LAlkoholname.setText("") - w.LMenge.setText("") - for field_ingredient, field_volume in zip(generate_maker_ingredients_fields(w), generate_maker_volume_fields(w)): - field_ingredient.setText("") - field_ingredient.setStyleSheet("color: rgb(0, 123, 255)") - field_volume.setText("") - - def clear_recipe_data_recipes(self, w, select_other_item): - w.LECocktail.clear() - w.LEKommentar.clear() - if not select_other_item: - w.LWRezepte.clearSelection() - self.set_multiple_combobox_to_top_item(generate_CBR_names(w)) - self.clean_multiple_lineedit(generate_lineedit_recipes(w)) - w.handaddlist = [] - - def refill_recipes_list_widget(self, w, items): - w.LWRezepte.clear() - self.fill_list_widget(w.LWRezepte, items) - - def remove_recipe_from_list_widgets(self, w, recipe_name): - self.delete_list_widget_item(w.LWRezepte, recipe_name) - self.delete_list_widget_item(w.LWMaker, recipe_name) - w.LWRezepte.clearSelection() - w.LWMaker.clearSelection() - - def set_recipe_handadd_comment(self, w, handadd_data): - comment = "" - for ingredient_name, volume, ingredient_id, alcoholic, alcohol_level in handadd_data: - comment += f"{volume} ml {ingredient_name}, " - w.handaddlist.append([ingredient_id, volume, alcoholic, 1, alcohol_level]) - comment = comment[:-2] - w.LEKommentar.setText(comment) - - def set_recipe_data(self, w, recipe_name, ingredient_names, ingredient_volumes, enaled, handadd_data): - if enaled: - w.CHBenabled.setChecked(True) - else: - w.CHBenabled.setChecked(False) - self.set_multiple_combobox_items(generate_CBR_names(w)[: len(ingredient_names)], ingredient_names) - self.fill_multiple_lineedit(generate_lineedit_recipes(w)[: len(ingredient_volumes)], ingredient_volumes) - w.LECocktail.setText(recipe_name) - self.set_recipe_handadd_comment(w, handadd_data) +import sys +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.uic import * + +from src.supporter import generate_maker_ingredients_fields, generate_maker_volume_fields, generate_CBR_names, generate_lineedit_recipes + + +class DisplayHandler: + """Handler Class to set/remove elements from to UI """ + + def __init__(self): + pass + + def standard_box(self, textstring): + """ The default messagebox for the Maker. Uses a QMessageBox with OK-Button """ + messagebox = QMessageBox() + messagebox.setStandardButtons(QMessageBox.Ok) + buttonok = messagebox.button(QMessageBox.Ok) + buttonok.setText(" OK ") + fillstring = "-" * 70 + messagebox.setText(f"{fillstring}\n{textstring}\n{fillstring}") + messagebox.setStyleSheet( + "QMessageBox QPushButton{background-color: rgb(0, 123, 255); color: rgb(0, 0, 0); font-size: 30pt;} QMessageBox{background-color: rgb(10, 10, 10); font-size: 16pt;} QMessageBox QLabel{color: rgb(0, 123, 255);}" + ) + messagebox.showFullScreen() + messagebox.exec_() + + # LineEdit + def clean_multiple_lineedit(self, lineedit_list): + for lineedit in lineedit_list: + lineedit.clear() + + def fill_multiple_lineedit(self, lineedit_list, text_list): + for lineedit, text in zip(lineedit_list, text_list): + lineedit.setText(str(text)) + + # Combobox + def fill_single_combobox(self, combobox, itemlist, clear_first=False, sort_items=True, first_empty=True): + if clear_first: + combobox.clear() + if combobox.count() == 0 and first_empty: + combobox.addItem("") + combobox.addItems(itemlist) + if sort_items: + combobox.model().sort(0) + + def fill_multiple_combobox(self, combobox_list, itemlist, clear_first=False, sort_items=True, first_empty=True): + for combobox in combobox_list: + self.fill_single_combobox(combobox, itemlist, clear_first, sort_items, first_empty) + + def fill_multiple_combobox_individually(self, combobox_list, list_of_itemlist, clear_first=False, sort_items=True, first_empty=True): + for combobox, itemlist in zip(combobox_list, list_of_itemlist): + self.fill_single_combobox(combobox, itemlist, clear_first, sort_items, first_empty) + + def delete_single_combobox_item(self, combobox, item): + index = combobox.findText(item, Qt.MatchFixedString) + if index >= 0: + combobox.removeItem(index) + + def delete_multiple_combobox_item(self, combobox, itemlist): + for item in itemlist: + self.delete_single_combobox_item(combobox, item) + + def delete_item_in_multiple_combobox(self, combobox_list, item): + for combobox in combobox_list: + self.delete_single_combobox_item(combobox, item) + + def sort_multiple_combobox(self, combobox_list): + for combobox in combobox_list: + combobox.sort() + + def set_multiple_combobox_to_top_item(self, combobox_list): + for combobox in combobox_list: + combobox.setCurrentIndex(0) + + def set_multiple_combobox_items(self, combobox_list, items_to_set): + for combobox, item in zip(combobox_list, items_to_set): + self.set_combobox_item(combobox, item) + + def set_combobox_item(self, combobox, item): + index = combobox.findText(item, Qt.MatchFixedString) + combobox.setCurrentIndex(index) + + def adjust_bottle_comboboxes(self, combobox_list, old_item, new_item): + for combobox in combobox_list: + if (old_item != "") and (combobox.findText(old_item, Qt.MatchFixedString) < 0): + combobox.addItem(old_item) + if (new_item != "") and (new_item != combobox.currentText()): + self.delete_single_combobox_item(combobox, new_item) + combobox.model().sort(0) + + def rename_single_combobox(self, combobox, old_item, new_item): + index = combobox.findText(old_item, Qt.MatchFixedString) + if index >= 0: + combobox.setItemText(index, new_item) + combobox.model().sort(0) + + def rename_multiple_combobox(self, combobox_list, old_item, new_item): + for combobox in combobox_list: + self.rename_single_combobox(combobox, old_item, new_item) + + # buttons / togglebuttons + def untoggle_buttons(self, button_list): + for button in button_list: + button.setChecked(False) + + # progress bars + def set_progress_bar_values(self, progress_bar_list, value_list): + for progress_bar, value in zip(progress_bar_list, value_list): + progress_bar.setValue(value) + + # listwidget + def unselect_list_widget_items(self, list_widget): + for i in range(list_widget.count()): + list_widget.item(i).setSelected(False) + + def delete_list_widget_item(self, list_widget, item): + index_to_delete = list_widget.findItems(item, Qt.MatchExactly) + if len(index_to_delete) > 0: + for index in index_to_delete: + list_widget.takeItem(list_widget.row(index)) + + def fill_list_widget(self, list_widget, item_list): + for item in item_list: + list_widget.addItem(item) + + # checkboxes + def set_checkbox_value(self, checkbox, value): + if value: + checkbox.setChecked(True) + else: + checkbox.setChecked(False) + + # label + def set_alcohol_level(self, w, value): + w.LAlkoholgehalt.setText(f"Alkohol: {value:.0f}%") + + # others + def fill_recipe_data_maker(self, w, display_data, total_volume, cocktailname): + w.LAlkoholname.setText(cocktailname) + w.LMenge.setText(f"Menge: {total_volume} ml") + fields_ingredient = generate_maker_ingredients_fields(w)[: len(display_data)] + fields_volume = generate_maker_volume_fields(w)[: len(display_data)] + for field_ingredient, field_volume, (ingredient_name, volume) in zip(fields_ingredient, fields_volume, display_data): + field_ingredient.setText(f"{ingredient_name} ") + if volume != "": + field_volume.setText(f" {volume} ml") + if ingredient_name == "Selbst hinzufügen:": + field_ingredient.setStyleSheet("color: rgb(170, 170, 170)") + + def clear_recipe_data_maker(self, w): + w.LAlkoholgehalt.setText("") + w.LAlkoholname.setText("") + w.LMenge.setText("") + for field_ingredient, field_volume in zip(generate_maker_ingredients_fields(w), generate_maker_volume_fields(w)): + field_ingredient.setText("") + field_ingredient.setStyleSheet("color: rgb(0, 123, 255)") + field_volume.setText("") + + def clear_recipe_data_recipes(self, w, select_other_item): + w.LECocktail.clear() + w.LEKommentar.clear() + if not select_other_item: + w.LWRezepte.clearSelection() + self.set_multiple_combobox_to_top_item(generate_CBR_names(w)) + self.clean_multiple_lineedit(generate_lineedit_recipes(w)) + w.handaddlist = [] + + def refill_recipes_list_widget(self, w, items): + w.LWRezepte.clear() + self.fill_list_widget(w.LWRezepte, items) + + def remove_recipe_from_list_widgets(self, w, recipe_name): + self.delete_list_widget_item(w.LWRezepte, recipe_name) + self.delete_list_widget_item(w.LWMaker, recipe_name) + w.LWRezepte.clearSelection() + w.LWMaker.clearSelection() + + def set_recipe_handadd_comment(self, w, handadd_data): + comment = "" + for ingredient_name, volume, ingredient_id, alcoholic, alcohol_level in handadd_data: + comment += f"{volume} ml {ingredient_name}, " + w.handaddlist.append([ingredient_id, volume, alcoholic, 1, alcohol_level]) + comment = comment[:-2] + w.LEKommentar.setText(comment) + + def set_recipe_data(self, w, recipe_name, ingredient_names, ingredient_volumes, enaled, handadd_data): + if enaled: + w.CHBenabled.setChecked(True) + else: + w.CHBenabled.setChecked(False) + self.set_multiple_combobox_items(generate_CBR_names(w)[: len(ingredient_names)], ingredient_names) + self.fill_multiple_lineedit(generate_lineedit_recipes(w)[: len(ingredient_volumes)], ingredient_volumes) + w.LECocktail.setText(recipe_name) + self.set_recipe_handadd_comment(w, handadd_data) From 24ed8e7773b55ae579b6bb722755a1c95e4da4ea Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 11 Oct 2021 21:12:32 +0200 Subject: [PATCH 06/65] finalized switch to new shared vars --- src/maker.py | 2 - src/rpi_controller.py | 14 +-- src_ui/setup_get_ingredients_window.py | 138 ++++++++++++------------- 3 files changed, 76 insertions(+), 78 deletions(-) diff --git a/src/maker.py b/src/maker.py index 93335be8..5304734d 100644 --- a/src/maker.py +++ b/src/maker.py @@ -157,8 +157,6 @@ def prepare_cocktail(w): w.tabWidget.setCurrentIndex(3) return - shared.cocktail_started = True - shared.make_cocktail = True consumption, taken_time, max_time = RPI_CONTROLLER.make_cocktail(w, ingredient_bottles, ingredient_volumes) DB_COMMANDER.set_recipe_counter(cocktailname) generate_maker_log_entry(cocktail_volume, cocktailname, taken_time, max_time) diff --git a/src/rpi_controller.py b/src/rpi_controller.py index cc5050c9..1d1ee6e7 100644 --- a/src/rpi_controller.py +++ b/src/rpi_controller.py @@ -32,7 +32,9 @@ def clean_pumps(self): qApp.processEvents() self.close_pinlist(active_pins) - def make_cocktail(self, w, bottle_list, volume_list, labelchange=""): + def make_cocktail(self, w, bottle_list: list[int], volume_list: list[float], labelchange=""): + shared.cocktail_started = True + shared.make_cocktail = True w.progressionqwindow(labelchange) already_closed_pins = set() indexes = [x - 1 for x in bottle_list] @@ -65,29 +67,29 @@ def make_cocktail(self, w, bottle_list, volume_list, labelchange=""): w.prow_close() return [round(x) for x in consumption], current_time, max_time - def close_pin(self, pin, current_time): + def close_pin(self, pin: int, current_time: float): if not self.devenvironment: GPIO.output(pin, 1) print(f"{current_time}s: Pin number <{pin}> is closed") - def activate_pinlist(self, pinlist): + def activate_pinlist(self, pinlist: list[int]): print(f"Opening Pins: {pinlist}") if not self.devenvironment: for pin in pinlist: GPIO.setup(pin, 0) GPIO.output(pin, 0) - def close_pinlist(self, pinlist): + def close_pinlist(self, pinlist: list[int]): print(f"Closing Pins: {pinlist}") if not self.devenvironment: for pin in pinlist: GPIO.output(pin, 1) - def consumption_print(self, consumption, current_time, max_time, interval=1): + def consumption_print(self, consumption: list[float], current_time: float, max_time: float, interval=1): if current_time % interval == 0: print( f"Making Cocktail, {current_time}/{max_time} s:\tThe consumption is currently {[round(x) for x in consumption]}") - def clean_print(self, t_cleaned, interval=2): + def clean_print(self, t_cleaned: float, interval=2): if t_cleaned % interval == 0: print(f"Cleaning, {t_cleaned}/{self.CLEAN_TIME} s\t{'.' * int(t_cleaned)}") diff --git a/src_ui/setup_get_ingredients_window.py b/src_ui/setup_get_ingredients_window.py index 26079fb3..d465815d 100644 --- a/src_ui/setup_get_ingredients_window.py +++ b/src_ui/setup_get_ingredients_window.py @@ -1,70 +1,68 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtGui import QIntValidator -from PyQt5.QtWidgets import * -from PyQt5.uic import * -import time - -from ui_elements.bonusingredient import Ui_addingredient - -from src.supporter import plusminus -from src.display_handler import DisplayHandler -from src.display_controller import DisplayController -from src.database_commander import DatabaseCommander -from src.rpi_controller import RpiController -from src.bottles import set_fill_level_bars - -DP_HANDLER = DisplayHandler() -DB_COMMANDER = DatabaseCommander() -RPI_CONTROLLER = RpiController() -DP_CONTROLLER = DisplayController() - - -class GetIngredientWindow(QDialog, Ui_addingredient): - """ Creates a Dialog to chose an additional ingredient and the amount - to spend this ingredient. - """ - - def __init__(self, parent=None): - """ Init. Connects all the buttons and get values for the Combobox. """ - super(GetIngredientWindow, self).__init__(parent) - self.setupUi(self) - # Set window properties - self.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint | Qt.WindowTitleHint | - Qt.WindowCloseButtonHint | Qt.WindowStaysOnTopHint) - self.setWindowIcon(QIcon(parent.icon_path)) - self.mainscreen = parent - if not self.mainscreen.DEVENVIRONMENT: - self.setCursor(Qt.BlankCursor) - # Connect all the buttons - self.PBplus.clicked.connect(lambda: plusminus(self.LAmount, "+", 20, 100, 10)) - self.PBminus.clicked.connect(lambda: plusminus(self.LAmount, "-", 20, 100, 10)) - self.PBAusgeben.clicked.connect(self.ausgeben_clicked) - self.PBAbbrechen.clicked.connect(self.abbrechen_clicked) - bottles = DB_COMMANDER.get_ingredients_at_bottles_without_empty_ones() - DP_HANDLER.fill_single_combobox(self.CBingredient, bottles, first_empty=False) - - def abbrechen_clicked(self): - """ Closes the Window without a change. """ - self.close() - - def ausgeben_clicked(self): - """ Calls the Progressbarwindow and spends the given amount of the ingredient. """ - import globalvars - - globalvars.make_cocktail = True - ingredient_name, volume = DP_CONTROLLER.get_data_ingredient_window(self) - bottle, level = DB_COMMANDER.get_ingredient_bottle_and_level_by_name(ingredient_name) - print(f"Ausgabemenge von {self.CBingredient.currentText()}: {volume}") - - self.close() - if volume > level: - DP_HANDLER.standard_box(f"{ingredient_name} hat nicht genug Volumen! {level}/{volume} ml vorhanden.") - self.mainscreen.tabWidget.setCurrentIndex(3) - return - - volume, _, _ = RPI_CONTROLLER.make_cocktail( - self.mainscreen, [bottle], [volume], labelchange="Zutat wird ausgegeben!\nFortschritt:") - DB_COMMANDER.set_ingredient_consumption(ingredient_name, volume[0]) - set_fill_level_bars(self.mainscreen) - self.mainscreen.prow_close() +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtGui import QIntValidator +from PyQt5.QtWidgets import * +from PyQt5.uic import * + +from ui_elements.bonusingredient import Ui_addingredient +from config.config_manager import shared + +from src.supporter import plusminus +from src.display_handler import DisplayHandler +from src.display_controller import DisplayController +from src.database_commander import DatabaseCommander +from src.rpi_controller import RpiController +from src.bottles import set_fill_level_bars + +DP_HANDLER = DisplayHandler() +DB_COMMANDER = DatabaseCommander() +RPI_CONTROLLER = RpiController() +DP_CONTROLLER = DisplayController() + + +class GetIngredientWindow(QDialog, Ui_addingredient): + """ Creates a Dialog to chose an additional ingredient and the amount + to spend this ingredient. + """ + + def __init__(self, parent=None): + """ Init. Connects all the buttons and get values for the Combobox. """ + super(GetIngredientWindow, self).__init__(parent) + self.setupUi(self) + # Set window properties + self.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint | Qt.WindowTitleHint | + Qt.WindowCloseButtonHint | Qt.WindowStaysOnTopHint) + self.setWindowIcon(QIcon(parent.icon_path)) + self.mainscreen = parent + if not self.mainscreen.DEVENVIRONMENT: + self.setCursor(Qt.BlankCursor) + # Connect all the buttons + self.PBplus.clicked.connect(lambda: plusminus(self.LAmount, "+", 20, 100, 10)) + self.PBminus.clicked.connect(lambda: plusminus(self.LAmount, "-", 20, 100, 10)) + self.PBAusgeben.clicked.connect(self.ausgeben_clicked) + self.PBAbbrechen.clicked.connect(self.abbrechen_clicked) + bottles = DB_COMMANDER.get_ingredients_at_bottles_without_empty_ones() + DP_HANDLER.fill_single_combobox(self.CBingredient, bottles, first_empty=False) + + def abbrechen_clicked(self): + """ Closes the Window without a change. """ + self.close() + + def ausgeben_clicked(self): + """ Calls the Progressbarwindow and spends the given amount of the ingredient. """ + ingredient_name, volume = DP_CONTROLLER.get_data_ingredient_window(self) + bottle, level = DB_COMMANDER.get_ingredient_bottle_and_level_by_name(ingredient_name) + print(f"Ausgabemenge von {self.CBingredient.currentText()}: {volume}") + + self.close() + if volume > level: + DP_HANDLER.standard_box(f"{ingredient_name} hat nicht genug Volumen! {level}/{volume} ml vorhanden.") + self.mainscreen.tabWidget.setCurrentIndex(3) + return + + volume, _, _ = RPI_CONTROLLER.make_cocktail( + self.mainscreen, [bottle], [volume], labelchange="Zutat wird ausgegeben!\nFortschritt:") + DB_COMMANDER.set_ingredient_consumption(ingredient_name, volume[0]) + set_fill_level_bars(self.mainscreen) + self.mainscreen.prow_close() + shared.cocktail_started = False From 1ced1e48f1dd9f984ef5cebd68c2e3573bec3fa2 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 11 Oct 2021 22:42:04 +0200 Subject: [PATCH 07/65] Added docs / example --- config/config_manager.py | 2 ++ microservice/.env.example | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/config_manager.py b/config/config_manager.py index 5694a425..785407a6 100644 --- a/config/config_manager.py +++ b/config/config_manager.py @@ -16,6 +16,8 @@ class ConfigManager: class Shared: + """Shared global variables which may dynamically change and are needed on different spaces""" + def __init__(self): self.cocktail_started = False self.make_cocktail = True diff --git a/microservice/.env.example b/microservice/.env.example index 96b16976..103329f6 100644 --- a/microservice/.env.example +++ b/microservice/.env.example @@ -1 +1,4 @@ -HOOK_ENDPOINT=enpointforhook \ No newline at end of file +HOOK_ENDPOINT=enpointforhook +SENDER_ADDRESS=cocktaimaker_adress +SENDER_PASSWORD=cocktailmaker_password +RECEIVER_ADRESS=receiver_adress \ No newline at end of file From 5c882e1048bfa050b7c5185bf218f840b1043a43 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 12 Oct 2021 15:55:21 +0200 Subject: [PATCH 08/65] Updated Date --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 09660284..a715a885 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Andre Wohnsland +Copyright (c) 2021 Andre Wohnsland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From e8fbad810afe403f18992a7a4af3ecb8ac3952a9 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 12 Oct 2021 15:56:53 +0200 Subject: [PATCH 09/65] Removed wildcard import, only importing needed --- src/asyncworker.py | 9 +++------ src/bottles.py | 9 ++------- src/display_controller.py | 12 ++++-------- src/display_handler.py | 8 +++----- src/ingredients.py | 5 ----- src/maker.py | 5 ----- src/recipes.py | 6 +----- src/rpi_controller.py | 5 +---- src_ui/setup_avialable_window.py | 7 +++---- src_ui/setup_bottle_window.py | 10 ++++------ src_ui/setup_get_ingredients_window.py | 8 +++----- src_ui/setup_handadd_widget.py | 9 ++++----- src_ui/setup_keyboard_widget.py | 5 +---- src_ui/setup_mainwindow.py | 6 ++---- src_ui/setup_password_screen.py | 7 +++---- src_ui/setup_progress_screen.py | 9 ++++----- 16 files changed, 38 insertions(+), 82 deletions(-) diff --git a/src/asyncworker.py b/src/asyncworker.py index c869eb73..dc6c58e6 100644 --- a/src/asyncworker.py +++ b/src/asyncworker.py @@ -1,10 +1,7 @@ import sys import traceback -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtCore import pyqtSignal, QRunnable, QObject, pyqtSlot class WorkerSignals(QObject): @@ -51,7 +48,7 @@ def __init__(self, fn, *args, **kwargs): super(Worker, self).__init__() # Store constructor arguments (re-used for processing) - self.fn = fn + self.function = fn self.args = args self.kwargs = kwargs self.signals = WorkerSignals() @@ -67,7 +64,7 @@ def run(self): # Retrieve args/kwargs here; and fire processing using them try: - result = self.fn(*self.args, **self.kwargs) + result = self.function(*self.args, **self.kwargs) except: traceback.print_exc() exctype, value = sys.exc_info()[:2] diff --git a/src/bottles.py b/src/bottles.py index 3ece146c..597b8b29 100644 --- a/src/bottles.py +++ b/src/bottles.py @@ -3,11 +3,6 @@ This includes all functions for the Lists, DB and Buttos/Dropdowns. """ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * - from config.config_manager import shared from src.error_suppression import logerror @@ -72,8 +67,8 @@ def calculate_combobox_bottles(w): shown_ingredients = [] for row, _ in enumerate(used_ingredients): - shown_ingredients.append(sorted(set(possible_ingredients) - - {x for i, x in enumerate(used_ingredients) if i != row})) + used_without_self = {x for i, x in enumerate(used_ingredients) if i != row} + shown_ingredients.append(sorted(set(possible_ingredients) - used_without_self)) DP_HANDLER.fill_multiple_combobox_individually(combobox_bottles, shown_ingredients, True) diff --git a/src/display_controller.py b/src/display_controller.py index 18e6c4b6..5940058a 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -1,8 +1,3 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * - from config.config_manager import ConfigManager from src.database_commander import DatabaseCommander from src.supporter import generate_lineedit_recipes, generate_CBR_names @@ -18,7 +13,7 @@ def get_current_combobox_items(self, combobox_list): return [combobox.currentText() for combobox in combobox_list] def get_toggle_status(self, button_list): - return [True if button.isChecked() else False for button in button_list] + return [button.isChecked() for button in button_list] def get_lineedit_text(self, lineedit_list): return [lineedit.text() for lineedit in lineedit_list] @@ -66,11 +61,12 @@ def check_ingredient_data(self, lineedit_list): missing_criteria = ["Der Zutatenname fehlt", "Der Alkoholgehalt fehlt", "Das Flaschenvolumen fehlt"] error_messages = self.missing_check(lineedit_list, missing_criteria) _, ingredient_percentage, ingredient_volume = lineedit_list - error_messages.extend(self.valid_check_int([ingredient_percentage, ingredient_volume], ["Alkoholgehalt", "Flaschenvolumen"])) + error_messages.extend(self.valid_check_int( + [ingredient_percentage, ingredient_volume], ["Alkoholgehalt", "Flaschenvolumen"])) try: if int(ingredient_percentage.text()) > 100: error_messages.append("Alkoholgehalt kann nicht größer als 100 sein!") - except: + except ValueError: pass return error_messages diff --git a/src/display_handler.py b/src/display_handler.py index d7ff9ede..0cc97945 100644 --- a/src/display_handler.py +++ b/src/display_handler.py @@ -1,8 +1,6 @@ -import sys -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox + from src.supporter import generate_maker_ingredients_fields, generate_maker_volume_fields, generate_CBR_names, generate_lineedit_recipes diff --git a/src/ingredients.py b/src/ingredients.py index 0bd2bfda..3194f032 100644 --- a/src/ingredients.py +++ b/src/ingredients.py @@ -2,11 +2,6 @@ """ Module with all nececcary functions for the ingredients Tab. This includes all functions for the Lists, DB and Buttos/Dropdowns. """ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * - from src.bottles import set_fill_level_bars, refresh_bottle_information from src.display_controller import DisplayController diff --git a/src/maker.py b/src/maker.py index 5304734d..34192a09 100644 --- a/src/maker.py +++ b/src/maker.py @@ -3,11 +3,6 @@ This includes all functions for the Lists, DB and Buttos/Dropdowns. """ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * - from src.bottles import set_fill_level_bars from src.error_suppression import logerror diff --git a/src/recipes.py b/src/recipes.py index 17f19743..29bd7f8e 100644 --- a/src/recipes.py +++ b/src/recipes.py @@ -4,10 +4,6 @@ """ from collections import Counter -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * from src.maker import refresh_recipe_maker_view from src.error_suppression import logerror @@ -62,7 +58,7 @@ def reason_check_ingredients(ingredient_names, ingredient_volumes): return [], [], f"Eine der Zutaten:\n<{double_names[0]}>\nwurde doppelt verwendet!" try: volumes = [int(x) for x in volumes] - except: + except ValueError: return [], [], "Menge muss eine Zahl sein!" return names, volumes, "" diff --git a/src/rpi_controller.py b/src/rpi_controller.py index 1d1ee6e7..c5538452 100644 --- a/src/rpi_controller.py +++ b/src/rpi_controller.py @@ -1,8 +1,5 @@ import time -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtWidgets import qApp from config.config_manager import shared from config.config_manager import ConfigManager diff --git a/src_ui/setup_avialable_window.py b/src_ui/setup_avialable_window.py index b5c20db8..99ee591c 100644 --- a/src_ui/setup_avialable_window.py +++ b/src_ui/setup_avialable_window.py @@ -1,7 +1,6 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMainWindow + from ui_elements.available import Ui_available from src.maker import refresh_recipe_maker_view diff --git a/src_ui/setup_bottle_window.py b/src_ui/setup_bottle_window.py index 2373e45a..39b0ca9a 100644 --- a/src_ui/setup_bottle_window.py +++ b/src_ui/setup_bottle_window.py @@ -1,7 +1,5 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMainWindow from ui_elements.bottlewindow import Ui_Bottlewindow @@ -38,9 +36,9 @@ def __init__(self, parent=None): mylabel = [getattr(self, f"LAmount{x}") for x in range(1, 11)] for plus, minus, field, vol in zip(myplus, myminus, mylabel, self.maxvolume): plus.clicked.connect(lambda _, l=field, b=vol: plusminus( - label=l, operator="+", minimal=50, maximal=b, dm=25)) + label=l, operator="+", minimal=50, maximal=b, delta=25)) minus.clicked.connect(lambda _, l=field, b=vol: plusminus( - label=l, operator="-", minimal=50, maximal=b, dm=25)) + label=l, operator="-", minimal=50, maximal=b, delta=25)) def abbrechen_clicked(self): """ Closes the Window without a change. """ diff --git a/src_ui/setup_get_ingredients_window.py b/src_ui/setup_get_ingredients_window.py index d465815d..86775688 100644 --- a/src_ui/setup_get_ingredients_window.py +++ b/src_ui/setup_get_ingredients_window.py @@ -1,8 +1,6 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtGui import QIntValidator -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QDialog from ui_elements.bonusingredient import Ui_addingredient from config.config_manager import shared diff --git a/src_ui/setup_handadd_widget.py b/src_ui/setup_handadd_widget.py index 764af94c..93ec6abb 100644 --- a/src_ui/setup_handadd_widget.py +++ b/src_ui/setup_handadd_widget.py @@ -1,9 +1,8 @@ from collections import Counter -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtGui import QIntValidator -from PyQt5.QtWidgets import * -from PyQt5.uic import * + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog +from PyQt5.QtGui import QIcon, QIntValidator from ui_elements.handadds import Ui_handadds from src.display_handler import DisplayHandler diff --git a/src_ui/setup_keyboard_widget.py b/src_ui/setup_keyboard_widget.py index 908d65f5..cc87904a 100644 --- a/src_ui/setup_keyboard_widget.py +++ b/src_ui/setup_keyboard_widget.py @@ -1,8 +1,5 @@ import string -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtWidgets import QDialog from ui_elements.Keyboard import Ui_Keyboard diff --git a/src_ui/setup_mainwindow.py b/src_ui/setup_mainwindow.py index f26ab6e4..3edfd674 100644 --- a/src_ui/setup_mainwindow.py +++ b/src_ui/setup_mainwindow.py @@ -2,11 +2,9 @@ of the passed window. Also defines the Mode for controls. """ import os -from PyQt5.QtCore import * -from PyQt5.QtGui import * +from PyQt5.QtCore import Qt from PyQt5.QtGui import QIntValidator -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtWidgets import QMainWindow from config.config_manager import ConfigManager from src.maker import * diff --git a/src_ui/setup_password_screen.py b/src_ui/setup_password_screen.py index 02e80081..78ceda1b 100644 --- a/src_ui/setup_password_screen.py +++ b/src_ui/setup_password_screen.py @@ -1,7 +1,6 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QDialog from ui_elements.passwordbuttons2 import Ui_PasswordWindow2 diff --git a/src_ui/setup_progress_screen.py b/src_ui/setup_progress_screen.py index e98f69ee..e3ac4838 100644 --- a/src_ui/setup_progress_screen.py +++ b/src_ui/setup_progress_screen.py @@ -1,7 +1,6 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.uic import * +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QMainWindow from ui_elements.progressbarwindow import Ui_Progressbarwindow @@ -15,7 +14,7 @@ def __init__(self, parent=None): super(ProgressScreen, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.FramelessWindowHint) - self.PBabbrechen.clicked.connect(lambda: interrupt_cocktail()) + self.PBabbrechen.clicked.connect(interrupt_cocktail) self.setWindowIcon(QIcon(parent.icon_path)) self.mainscreen = parent if not self.mainscreen.DEVENVIRONMENT: From 173585728f292bff823cdcb5725f99da33981cfc Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 12 Oct 2021 15:57:24 +0200 Subject: [PATCH 10/65] Minor linting fixes --- src/error_suppression.py | 2 +- src/save_handler.py | 5 +++-- src/service_handler.py | 2 +- src/supporter.py | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/error_suppression.py b/src/error_suppression.py index 296f2d70..270cf345 100644 --- a/src/error_suppression.py +++ b/src/error_suppression.py @@ -16,7 +16,7 @@ def wrapper(*args, **kwargs): func(*args, **kwargs) except Exception: logger.exception("The function %s could not be fully excecuted!", func.__name__) - print("The function {} could not be fully excecuted!".format(func.__name__)) + print(f"The function {func.__name__} could not be fully excecuted!") else: func(*args, **kwargs) diff --git a/src/save_handler.py b/src/save_handler.py index f4307d79..11bff344 100644 --- a/src/save_handler.py +++ b/src/save_handler.py @@ -46,8 +46,9 @@ def write_rows_to_csv(self, filename, data_rows): subfoldername = "saves" full_file_name = f"{dtime}_{filename}" savepath = os.path.join(DIRPATH, "..", subfoldername, full_file_name) - with open(savepath, mode="a", newline="") as writer_file: + with open(savepath, mode="a", newline="", encoding="utf-8") as writer_file: csv_writer = csv.writer(writer_file, delimiter=",") for row in data_rows: csv_writer.writerow(row) - S_HANDLER.send_mail(full_file_name, open(savepath, "rb")) + with open(savepath, "rb") as read_file: + S_HANDLER.send_mail(full_file_name, read_file) diff --git a/src/service_handler.py b/src/service_handler.py index cd814e9e..257b5279 100644 --- a/src/service_handler.py +++ b/src/service_handler.py @@ -19,7 +19,7 @@ def log_connection_error(self, func: str): def post_cocktail_to_hook(self, cocktailname: str, cocktail_volume: int) -> Dict: if not self.USE_MICROSERVICE: return return_service_disabled() - # calculare volume in litre + # calculate volume in litre payload = json.dumps({"cocktailname": cocktailname, "volume": cocktail_volume / 1000}) endpoint = "/hookhandler/cocktail" full_url = f"{self.base_url}{endpoint}" diff --git a/src/supporter.py b/src/supporter.py index b5a456f9..e5029c53 100644 --- a/src/supporter.py +++ b/src/supporter.py @@ -1,9 +1,9 @@ -def plusminus(label, operator, minimal=0, maximal=1000, dm=10): +def plusminus(label, operator, minimal=0, maximal=1000, delta=10): """ increases or decreases the value by a given amount in the boundaries""" try: value_ = int(label.text()) - value_ = value_ + (dm if operator == "+" else -dm) - value_ = min(maximal, max(minimal, (value_ // dm) * dm)) + value_ = value_ + (delta if operator == "+" else -delta) + value_ = min(maximal, max(minimal, (value_ // delta) * delta)) except ValueError: value_ = maximal if operator == "+" else minimal label.setText(str(value_)) From 578e184e84fd11ad4964895784bfc8453aeb9ad7 Mon Sep 17 00:00:00 2001 From: Andre Date: Wed, 13 Oct 2021 19:41:39 +0200 Subject: [PATCH 11/65] Added comments and new config options --- config/config_manager.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/config_manager.py b/config/config_manager.py index 785407a6..458c64d4 100644 --- a/config/config_manager.py +++ b/config/config_manager.py @@ -1,17 +1,32 @@ class ConfigManager: """Manager for all static configuration of the machine """ + # Password to lock clean, delete and other critical operators MASTERPASSWORD = "1337" + # RPi pins where pumps (ascending) are connected USEDPINS = [14, 15, 18, 23, 24, 25, 8, 7, 17, 27, 22, 20] + # Volumeflow for the according pumps PUMP_VOLUMEFLOW = [30, 30, 25, 30, 30, 30, 25, 30, 30, 23, 30, 30] + # Number of bottles possible at the machine NUMBER_BOTTLES = 10 + # Time in seconds to execute clean programm CLEAN_TIME = 20 + # time between each check loop when making cocktail SLEEP_TIME = 0.05 + # Locks the recipe tab, making it impossible to acesss PARTYMODE = False + # Names for the according logger files LOGGERNAME = "cocktaillogger" LOGGERNAME_DEBUG = "debuglogger" + # If to use microservice (mostly docker on same device) to handle external API calls and according url USE_MICROSERVICE = False MICROSERVICE_BASE_URL = "http://127.0.0.1:5000" + # if to use the teams function and according options. + # URL should be 'device_ip:8080' where dashboard container is running and in the same network + USE_TEAMS = True + TEAM_BUTTON_NAMES = ["Team 1", "Team 2"] + TEAM_API_URL = "http://127.0.0.1:8080" + # Activating some dev features like mouse cursor DEVENVIRONMENT = True From 9bfa183d5ad33a7ccd54aa8e2f2bf85ff8ff0f27 Mon Sep 17 00:00:00 2001 From: Andre Date: Wed, 13 Oct 2021 19:41:50 +0200 Subject: [PATCH 12/65] Ignore testing team db --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6a0ae687..f39c7cf8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ venv *.csv __pycache__ .env -.venv \ No newline at end of file +.venv +team.db \ No newline at end of file From 1e0c2051978a5c2cceb1f76f0606088aa7b2ed7f Mon Sep 17 00:00:00 2001 From: Andre Date: Wed, 13 Oct 2021 19:42:54 +0200 Subject: [PATCH 13/65] Added docker and requirements for dashboard --- dashboard/backend/Dockerfile | 12 + dashboard/backend/requirements.txt | 2 + dashboard/docker-compose.yaml | 17 + dashboard/frontend/Dockerfile | 12 + dashboard/frontend/requirements.txt | 4 + dashboard/poetry.lock | 2058 +++++++++++++++++++++++++++ dashboard/pyproject.toml | 21 + dashboard/storage/.gitkeep | 0 8 files changed, 2126 insertions(+) create mode 100644 dashboard/backend/Dockerfile create mode 100644 dashboard/backend/requirements.txt create mode 100644 dashboard/docker-compose.yaml create mode 100644 dashboard/frontend/Dockerfile create mode 100644 dashboard/frontend/requirements.txt create mode 100644 dashboard/poetry.lock create mode 100644 dashboard/pyproject.toml create mode 100644 dashboard/storage/.gitkeep diff --git a/dashboard/backend/Dockerfile b/dashboard/backend/Dockerfile new file mode 100644 index 00000000..e85ead33 --- /dev/null +++ b/dashboard/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.8-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +EXPOSE 8080 + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/dashboard/backend/requirements.txt b/dashboard/backend/requirements.txt new file mode 100644 index 00000000..8353fce9 --- /dev/null +++ b/dashboard/backend/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.70.0 +uvicorn==0.15.0 diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml new file mode 100644 index 00000000..c74bf782 --- /dev/null +++ b/dashboard/docker-compose.yaml @@ -0,0 +1,17 @@ +version: '3' + +services: + frontend: + build: frontend + ports: + - 8501:8501 + depends_on: + - backend + volumes: + - ./storage:/app/storage + backend: + build: backend + ports: + - 8080:8080 + volumes: + - ./storage:/app/storage diff --git a/dashboard/frontend/Dockerfile b/dashboard/frontend/Dockerfile new file mode 100644 index 00000000..1e59dd40 --- /dev/null +++ b/dashboard/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.8-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +EXPOSE 8501 + +CMD ["streamlit", "run", "main.py", "--theme.base", "dark"] diff --git a/dashboard/frontend/requirements.txt b/dashboard/frontend/requirements.txt new file mode 100644 index 00000000..48302bbd --- /dev/null +++ b/dashboard/frontend/requirements.txt @@ -0,0 +1,4 @@ +streamlit==1.0.0 +streamlit-autorefresh == 0.0.1 +matplotlib == 3.4.3 +pywaffle == 0.6.3 \ No newline at end of file diff --git a/dashboard/poetry.lock b/dashboard/poetry.lock new file mode 100644 index 00000000..3da89df7 --- /dev/null +++ b/dashboard/poetry.lock @@ -0,0 +1,2058 @@ +[[package]] +name = "altair" +version = "4.1.0" +description = "Altair: A declarative statistical visualization library for Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +entrypoints = "*" +jinja2 = "*" +jsonschema = "*" +numpy = "*" +pandas = ">=0.18" +toolz = "*" + +[package.extras] +dev = ["black", "docutils", "ipython", "flake8", "pytest", "sphinx", "m2r", "vega-datasets", "recommonmark"] + +[[package]] +name = "anyio" +version = "3.3.3" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "appnope" +version = "0.1.2" +description = "Disable App Nap on macOS >= 10.9" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "argon2-cffi" +version = "21.1.0" +description = "The secure Argon2 password hashing algorithm." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +cffi = ">=1.0.0" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "furo", "wheel", "pre-commit"] +docs = ["sphinx", "furo"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] + +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "astor" +version = "0.8.1" +description = "Read/rewrite/write Python ASTs" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "backports.zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tzdata = ["tzdata"] + +[[package]] +name = "base58" +version = "2.1.0" +description = "Base58 and Base58Check implementation." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +tests = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "PyHamcrest (>=2.0.2)", "coveralls", "pytest-benchmark"] + +[[package]] +name = "bleach" +version = "4.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + +[[package]] +name = "blinker" +version = "1.4" +description = "Fast, simple object-to-object and broadcast signaling" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cachetools" +version = "4.2.4" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = false +python-versions = "~=3.5" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.14.6" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "cycler" +version = "0.10.0" +description = "Composable style cycles" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[[package]] +name = "debugpy" +version = "1.5.0" +description = "An implementation of the Debug Adapter Protocol for Python" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[[package]] +name = "decorator" +version = "5.1.0" +description = "Decorators for Humans" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "entrypoints" +version = "0.3" +description = "Discover and load entry points from installed packages." +category = "main" +optional = false +python-versions = ">=2.7" + +[[package]] +name = "fastapi" +version = "0.70.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.16.0" + +[package.extras] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] + +[[package]] +name = "gitdb" +version = "4.0.7" +description = "Git Object Database" +category = "main" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<5" + +[[package]] +name = "gitpython" +version = "3.1.24" +description = "GitPython is a python library used to interact with Git repositories" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "idna" +version = "3.2" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ipykernel" +version = "6.4.1" +description = "IPython Kernel for Jupyter" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +debugpy = ">=1.0.0,<2.0" +ipython = ">=7.23.1,<8.0" +ipython-genutils = "*" +jupyter-client = "<8.0" +matplotlib-inline = ">=0.1.0,<0.2.0" +tornado = ">=4.2,<7.0" +traitlets = ">=4.1.0,<6.0" + +[package.extras] +test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "ipyparallel"] + +[[package]] +name = "ipython" +version = "7.28.0" +description = "IPython: Productive Interactive Computing" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +traitlets = ">=4.2" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "ipywidgets" +version = "7.6.5" +description = "IPython HTML widgets for Jupyter" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ipykernel = ">=4.5.1" +ipython = {version = ">=4.0.0", markers = "python_version >= \"3.3\""} +ipython-genutils = ">=0.2.0,<0.3.0" +jupyterlab-widgets = {version = ">=1.0.0", markers = "python_version >= \"3.6\""} +nbformat = ">=4.2.0" +traitlets = ">=4.3.1" +widgetsnbextension = ">=3.5.0,<3.6.0" + +[package.extras] +test = ["pytest (>=3.6.0)", "pytest-cov", "mock"] + +[[package]] +name = "jedi" +version = "0.18.0" +description = "An autocompletion tool for Python that can be used for text editors." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] + +[[package]] +name = "jinja2" +version = "3.0.2" +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 = "jsonschema" +version = "4.1.0" +description = "An implementation of JSON Schema validation for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format_nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jupyter-client" +version = "7.0.6" +description = "Jupyter protocol implementation and client libraries" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +entrypoints = "*" +jupyter-core = ">=4.6.0" +nest-asyncio = ">=1.5" +python-dateutil = ">=2.1" +pyzmq = ">=13" +tornado = ">=4.1" +traitlets = "*" + +[package.extras] +doc = ["myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +test = ["codecov", "coverage", "ipykernel", "ipython", "mock", "mypy", "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", "pytest-timeout", "jedi (<0.18)"] + +[[package]] +name = "jupyter-core" +version = "4.8.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = "*" + +[[package]] +name = "jupyterlab-pygments" +version = "0.1.2" +description = "Pygments theme using JupyterLab CSS variables" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pygments = ">=2.4.1,<3" + +[[package]] +name = "jupyterlab-widgets" +version = "1.0.2" +description = "A JupyterLab extension." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "kiwisolver" +version = "1.3.2" +description = "A fast implementation of the Cassowary constraint solver" +category = "main" +optional = false +python-versions = ">=3.7" + +[[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" +version = "3.4.3" +description = "Python plotting package" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cycler = ">=0.10" +kiwisolver = ">=1.0.1" +numpy = ">=1.16" +pillow = ">=6.2.0" +pyparsing = ">=2.2.1" +python-dateutil = ">=2.7" + +[[package]] +name = "matplotlib-inline" +version = "0.1.3" +description = "Inline Matplotlib backend for Jupyter" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mistune" +version = "0.8.4" +description = "The fastest markdown parser in pure Python" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "nbclient" +version = "0.5.4" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +jupyter-client = ">=6.1.5" +nbformat = ">=5.0" +nest-asyncio = "*" +traitlets = ">=4.2" + +[package.extras] +dev = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "check-manifest", "flake8", "mypy", "tox", "bumpversion", "xmltodict", "pip (>=18.1)", "wheel (>=0.31.0)", "setuptools (>=38.6.0)", "twine (>=1.11.0)", "black"] +sphinx = ["Sphinx (>=1.7)", "sphinx-book-theme", "mock", "moto", "myst-parser"] +test = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "check-manifest", "flake8", "mypy", "tox", "bumpversion", "xmltodict", "pip (>=18.1)", "wheel (>=0.31.0)", "setuptools (>=38.6.0)", "twine (>=1.11.0)", "black"] + +[[package]] +name = "nbconvert" +version = "6.2.0" +description = "Converting Jupyter Notebooks" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +bleach = "*" +defusedxml = "*" +entrypoints = ">=0.2.2" +jinja2 = ">=2.4" +jupyter-core = "*" +jupyterlab-pygments = "*" +mistune = ">=0.8.1,<2" +nbclient = ">=0.5.0,<0.6.0" +nbformat = ">=4.4" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +testpath = "*" +traitlets = ">=5.0" + +[package.extras] +all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] +docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] +serve = ["tornado (>=4.0)"] +test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)"] +webpdf = ["pyppeteer (==0.2.6)"] + +[[package]] +name = "nbformat" +version = "5.1.3" +description = "The Jupyter Notebook format" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +ipython-genutils = "*" +jsonschema = ">=2.4,<2.5.0 || >2.5.0" +jupyter-core = "*" +traitlets = ">=4.1" + +[package.extras] +fast = ["fastjsonschema"] +test = ["check-manifest", "fastjsonschema", "testpath", "pytest", "pytest-cov"] + +[[package]] +name = "nest-asyncio" +version = "1.5.1" +description = "Patch asyncio to allow nested event loops" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "notebook" +version = "6.4.4" +description = "A web-based notebook environment for interactive computing" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +argon2-cffi = "*" +ipykernel = "*" +ipython-genutils = "*" +jinja2 = "*" +jupyter-client = ">=5.3.4" +jupyter-core = ">=4.6.1" +nbconvert = "*" +nbformat = "*" +prometheus-client = "*" +pyzmq = ">=17" +Send2Trash = ">=1.5.0" +terminado = ">=0.8.3" +tornado = ">=6.1" +traitlets = ">=4.2.1" + +[package.extras] +docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "myst-parser"] +json-logging = ["json-logging"] +test = ["pytest", "coverage", "requests", "nbval", "selenium", "pytest-cov", "requests-unixsocket"] + +[[package]] +name = "numpy" +version = "1.21.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pandas" +version = "1.3.3" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.7.1" + +[package.dependencies] +numpy = ">=1.17.3" +python-dateutil = ">=2.7.3" +pytz = ">=2017.3" + +[package.extras] +test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"] + +[[package]] +name = "pandocfilters" +version = "1.5.0" +description = "Utilities for writing pandoc filters in python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "parso" +version = "0.8.2" +description = "A Python Parser" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pillow" +version = "8.3.2" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pip-chill" +version = "1.0.1" +description = "Like `pip freeze` but lists only the packages that are not dependencies of installed packages." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "prometheus-client" +version = "0.11.0" +description = "Python client for the Prometheus monitoring system." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.20" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "protobuf" +version = "3.18.1" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "py" +version = "1.10.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.*" + +[[package]] +name = "pyarrow" +version = "5.0.0" +description = "Python library for Apache Arrow" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +numpy = ">=1.16.6" + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydeck" +version = "0.7.0" +description = "Widget for deck.gl maps" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +ipykernel = {version = ">=5.1.2", markers = "python_version >= \"3.4\""} +ipywidgets = ">=7.0.0" +jinja2 = ">=2.10.1" +numpy = ">=1.16.4" +traitlets = ">=4.3.2" + +[package.extras] +testing = ["pytest"] + +[[package]] +name = "pygments" +version = "2.10.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pyrsistent" +version = "0.18.0" +description = "Persistent/Functional/Immutable data structures" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2021.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pywaffle" +version = "0.6.3" +description = "PyWaffle is an open source, MIT-licensed Python package for plotting waffle charts." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +matplotlib = "*" + +[[package]] +name = "pywin32" +version = "302" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pywinpty" +version = "1.1.4" +description = "Pseudo terminal support for Windows from Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyzmq" +version = "22.3.0" +description = "Python bindings for 0MQ" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} +py = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "send2trash" +version = "1.8.0" +description = "Send file to trash natively under Mac OS X, Windows and Linux." +category = "main" +optional = false +python-versions = "*" + +[package.extras] +nativelib = ["pyobjc-framework-cocoa", "pywin32"] +objc = ["pyobjc-framework-cocoa"] +win32 = ["pywin32"] + +[[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 = "smmap" +version = "4.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "starlette" +version = "0.16.0" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] + +[[package]] +name = "streamlit" +version = "1.0.0" +description = "The fastest way to build data apps in Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +altair = ">=3.2.0" +astor = "*" +attrs = "*" +base58 = "*" +blinker = "*" +cachetools = ">=4.0" +click = ">=7.0,<8.0" +gitpython = "!=3.1.19" +numpy = "*" +packaging = "*" +pandas = ">=0.21.0" +pillow = ">=6.2.0" +protobuf = ">=3.6.0,<3.11 || >3.11" +pyarrow = "*" +pydeck = ">=0.1.dev5" +python-dateutil = "*" +requests = "*" +toml = "*" +tornado = ">=5.0" +tzlocal = "*" +validators = "*" +watchdog = {version = "*", markers = "platform_system != \"Darwin\""} + +[[package]] +name = "streamlit-autorefresh" +version = "0.0.1" +description = "Simple way to autorefresh your Streamlit apps" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +streamlit = ">=0.75" + +[[package]] +name = "terminado" +version = "0.12.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "testpath" +version = "0.5.0" +description = "Test utilities for code working with files and commands" +category = "main" +optional = false +python-versions = ">= 3.5" + +[package.extras] +test = ["pytest", "pathlib2"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toolz" +version = "0.11.1" +description = "List processing tools and functional utilities" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "tornado" +version = "6.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "main" +optional = false +python-versions = ">= 3.5" + +[[package]] +name = "traitlets" +version = "5.1.0" +description = "Traitlets Python configuration system" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "tzdata" +version = "2021.2.post0" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" + +[[package]] +name = "tzlocal" +version = "3.0" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"] + +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "uvicorn" +version = "0.15.0" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] + +[[package]] +name = "validators" +version = "0.18.2" +description = "Python Data Validation for Humans™." +category = "main" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +decorator = ">=3.4.0" +six = ">=1.4.0" + +[package.extras] +test = ["pytest (>=2.2.3)", "flake8 (>=2.4.0)", "isort (>=4.2.2)"] + +[[package]] +name = "watchdog" +version = "2.1.6" +description = "Filesystem events monitoring" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "widgetsnbextension" +version = "3.5.1" +description = "IPython HTML widgets for Jupyter" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +notebook = ">=4.4.1" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "d7013cef5fa9b5a36631b4b407b42d681be32f8c7ac8a86498c7e447e1095abb" + +[metadata.files] +altair = [ + {file = "altair-4.1.0-py3-none-any.whl", hash = "sha256:7748841a1bea8354173d1140bef6d3b58bea21d201f562528e9599ea384feb7f"}, + {file = "altair-4.1.0.tar.gz", hash = "sha256:3edd30d4f4bb0a37278b72578e7e60bc72045a8e6704179e2f4738e35bc12931"}, +] +anyio = [ + {file = "anyio-3.3.3-py3-none-any.whl", hash = "sha256:56ceaeed2877723578b1341f4f68c29081db189cfb40a97d1922b9513f6d7db6"}, + {file = "anyio-3.3.3.tar.gz", hash = "sha256:8eccec339cb4a856c94a75d50fc1d451faf32a05ef406be462e2efc59c9838b0"}, +] +appnope = [ + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, +] +argon2-cffi = [ + {file = "argon2-cffi-21.1.0.tar.gz", hash = "sha256:f710b61103d1a1f692ca3ecbd1373e28aa5e545ac625ba067ff2feca1b2bb870"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-macosx_10_14_x86_64.whl", hash = "sha256:217b4f0f853ccbbb5045242946ad2e162e396064575860141b71a85eb47e475a"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa7e7d1fc22514a32b1761fdfa1882b6baa5c36bb3ef557bdd69e6fc9ba14a41"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-win32.whl", hash = "sha256:e4d8f0ae1524b7b0372a3e574a2561cbdddb3fdb6c28b70a72868189bda19659"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-win_amd64.whl", hash = "sha256:65213a9174320a1aee03fe826596e0620783966b49eb636955958b3074e87ff9"}, + {file = "argon2_cffi-21.1.0-pp36-pypy36_pp73-macosx_10_7_x86_64.whl", hash = "sha256:245f64a203012b144b7b8c8ea6d468cb02b37caa5afee5ba4a10c80599334f6a"}, + {file = "argon2_cffi-21.1.0-pp36-pypy36_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4ad152c418f7eb640eac41ac815534e6aa61d1624530b8e7779114ecfbf327f8"}, + {file = "argon2_cffi-21.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:bc513db2283c385ea4da31a2cd039c33380701f376f4edd12fe56db118a3b21a"}, + {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c7a7c8cc98ac418002090e4add5bebfff1b915ea1cb459c578cd8206fef10378"}, + {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:165cadae5ac1e26644f5ade3bd9c18d89963be51d9ea8817bd671006d7909057"}, + {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:566ffb581bbd9db5562327aee71b2eda24a1c15b23a356740abe3c011bbe0dcb"}, +] +asgiref = [ + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] +astor = [ + {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, + {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +"backports.zoneinfo" = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] +base58 = [ + {file = "base58-2.1.0-py3-none-any.whl", hash = "sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48"}, + {file = "base58-2.1.0.tar.gz", hash = "sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2"}, +] +bleach = [ + {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, + {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, +] +blinker = [ + {file = "blinker-1.4.tar.gz", hash = "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"}, +] +cachetools = [ + {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"}, + {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +cffi = [ + {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, + {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, + {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, + {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, + {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, + {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, + {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, + {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, + {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, + {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, + {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, + {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, + {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, + {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, + {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, + {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, + {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, + {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, + {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +cycler = [ + {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, + {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, +] +debugpy = [ + {file = "debugpy-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:098753d30232d1e4264eee37e1ddd5d106dc5c4bc6d8d7f4dadad9e44736cd48"}, + {file = "debugpy-1.5.0-cp310-cp310-win32.whl", hash = "sha256:33e8a9b4949be8b4f5fcfff07e24bd63c565060659f1c79773c08d19eee012f2"}, + {file = "debugpy-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef71eb8eb276370f8e74ab3f8c7648bbdc9aabac814a5b2840c8dd38a7bc7251"}, + {file = "debugpy-1.5.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:dd0e8d5e099444c22b27511dafd48e8bdcd7051b811ddd0ab2062965fe36ac80"}, + {file = "debugpy-1.5.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:990228f15de4ccbc52c2accf41a63b3b8d0a01e3de9876e02e77e487c4b1ffab"}, + {file = "debugpy-1.5.0-cp36-cp36m-win32.whl", hash = "sha256:77b5233b23a248cd930bf03ecd684da065c6e7d2a57d137516b6fa1698a58317"}, + {file = "debugpy-1.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c3184666cfe1768bf110f8075bafea59d2afce3cc54f4c501f2371c7238bc69d"}, + {file = "debugpy-1.5.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1283e418f595262d11abc5fae6a3ac629c5fc3b44d3988511ea755414aab3062"}, + {file = "debugpy-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a03051ba4fdf6720ee83a42e9f803e3a0b69a48b00436b97d16aeda49d28a8bf"}, + {file = "debugpy-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:cdaf6baaf8176644e752aed321b3f810dcf8b0439709f7edd9ae542f849a639b"}, + {file = "debugpy-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:be7ca2baef5a634dfbd086d9c1d6b5e0783c6d0f6d0a004b43d36f625d4fc0a9"}, + {file = "debugpy-1.5.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:72093ea83226d5264b3697b948c07a3cfcc4953da14a78a50c4e623a2bb99ad8"}, + {file = "debugpy-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ce0794d50391c87813bb148548c34dc638fb4d58198d275334968f63c088aa69"}, + {file = "debugpy-1.5.0-cp38-cp38-win32.whl", hash = "sha256:de56775b3dbbfc02bc9fb0682da4a960e0a5bada699eac5e22e0723c4107ec9f"}, + {file = "debugpy-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:82c4fa1293981a28c435d196a3714e06df761daff0da3336234475ceff1b042c"}, + {file = "debugpy-1.5.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:8e7391a08a351adce6e5154ed35e4cf90c5f3c10dbf7c8f6a234faef300588d6"}, + {file = "debugpy-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dacdb0a3377063d638bd8736c80b7274ae341ce778fec0f883ef1cbb79538bf2"}, + {file = "debugpy-1.5.0-cp39-cp39-win32.whl", hash = "sha256:fda623aa1036b34d554a1225a09cae6bf02b06c0ad903a9f0b8ac3cb74eddc15"}, + {file = "debugpy-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:9f3bed64027bd80a8fe1f35491ec0ec2d2c85f1e63dac7c0311e400bfe58cf05"}, + {file = "debugpy-1.5.0-py2.py3-none-any.whl", hash = "sha256:f058c204341fd7ff800ee0edafc106ca0fb1c9857e8a8895a6e04cca3ddcb7bf"}, + {file = "debugpy-1.5.0.zip", hash = "sha256:86febd61fc351cee926060eef008e242b7259957d71d25eef82860d0cc59b4dc"}, +] +decorator = [ + {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, + {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, +] +defusedxml = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] +fastapi = [ + {file = "fastapi-0.70.0-py3-none-any.whl", hash = "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c"}, + {file = "fastapi-0.70.0.tar.gz", hash = "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced"}, +] +gitdb = [ + {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, + {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, +] +gitpython = [ + {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, + {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, +] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +idna = [ + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, +] +ipykernel = [ + {file = "ipykernel-6.4.1-py3-none-any.whl", hash = "sha256:a3f6c2dda2ecf63b37446808a70ed825fea04790779ca524889c596deae0def8"}, + {file = "ipykernel-6.4.1.tar.gz", hash = "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd"}, +] +ipython = [ + {file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"}, + {file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +ipywidgets = [ + {file = "ipywidgets-7.6.5-py2.py3-none-any.whl", hash = "sha256:d258f582f915c62ea91023299603be095de19afb5ee271698f88327b9fe9bf43"}, + {file = "ipywidgets-7.6.5.tar.gz", hash = "sha256:00974f7cb4d5f8d494c19810fedb9fa9b64bffd3cda7c2be23c133a1ad3c99c5"}, +] +jedi = [ + {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, + {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, +] +jinja2 = [ + {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, + {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, +] +jsonschema = [ + {file = "jsonschema-4.1.0-py3-none-any.whl", hash = "sha256:2b3cca28580511d44326f0e7fc582eab3cbe31aabd1a1c2cfa74a399796ffd84"}, + {file = "jsonschema-4.1.0.tar.gz", hash = "sha256:9dd7c33b4a96138dc37bb86b3610d3b12d30d96433d4d73435ca3025804154a8"}, +] +jupyter-client = [ + {file = "jupyter_client-7.0.6-py3-none-any.whl", hash = "sha256:074bdeb1ffaef4a3095468ee16313938cfdc48fc65ca95cc18980b956c2e5d79"}, + {file = "jupyter_client-7.0.6.tar.gz", hash = "sha256:8b6e06000eb9399775e0a55c52df6c1be4766666209c22f90c2691ded0e338dc"}, +] +jupyter-core = [ + {file = "jupyter_core-4.8.1-py3-none-any.whl", hash = "sha256:8dd262ec8afae95bd512518eb003bc546b76adbf34bf99410e9accdf4be9aa3a"}, + {file = "jupyter_core-4.8.1.tar.gz", hash = "sha256:ef210dcb4fca04de07f2ead4adf408776aca94d17151d6f750ad6ded0b91ea16"}, +] +jupyterlab-pygments = [ + {file = "jupyterlab_pygments-0.1.2-py2.py3-none-any.whl", hash = "sha256:abfb880fd1561987efaefcb2d2ac75145d2a5d0139b1876d5be806e32f630008"}, + {file = "jupyterlab_pygments-0.1.2.tar.gz", hash = "sha256:cfcda0873626150932f438eccf0f8bf22bfa92345b814890ab360d666b254146"}, +] +jupyterlab-widgets = [ + {file = "jupyterlab_widgets-1.0.2-py3-none-any.whl", hash = "sha256:f5d9efface8ec62941173ba1cffb2edd0ecddc801c11ae2931e30b50492eb8f7"}, + {file = "jupyterlab_widgets-1.0.2.tar.gz", hash = "sha256:7885092b2b96bf189c3a705cc3c412a4472ec5e8382d0b47219a66cccae73cfa"}, +] +kiwisolver = [ + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"}, + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0"}, + {file = "kiwisolver-1.3.2-cp310-cp310-win32.whl", hash = "sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977"}, + {file = "kiwisolver-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-win32.whl", hash = "sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28"}, + {file = "kiwisolver-1.3.2-cp38-cp38-win32.whl", hash = "sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c"}, + {file = "kiwisolver-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439"}, + {file = "kiwisolver-1.3.2-cp39-cp39-win32.whl", hash = "sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9"}, + {file = "kiwisolver-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560"}, + {file = "kiwisolver-1.3.2.tar.gz", hash = "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c"}, +] +markupsafe = [ + {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-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-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_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-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-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 = [ + {file = "matplotlib-3.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913"}, + {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1c5efc278d996af8a251b2ce0b07bbeccb821f25c8c9846bdcb00ffc7f158aa"}, + {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eeb1859efe7754b1460e1d4991bbd4a60a56f366bc422ef3a9c5ae05f0bc70b5"}, + {file = "matplotlib-3.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:844a7b0233e4ff7fba57e90b8799edaa40b9e31e300b8d5efc350937fa8b1bea"}, + {file = "matplotlib-3.4.3-cp37-cp37m-win32.whl", hash = "sha256:85f0c9cf724715e75243a7b3087cf4a3de056b55e05d4d76cc58d610d62894f3"}, + {file = "matplotlib-3.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c70b6311dda3e27672f1bf48851a0de816d1ca6aaf3d49365fbdd8e959b33d2b"}, + {file = "matplotlib-3.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b884715a59fec9ad3b6048ecf3860f3b2ce965e676ef52593d6fa29abcf7d330"}, + {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a78a3b51f29448c7f4d4575e561f6b0dbb8d01c13c2046ab6c5220eb25c06506"}, + {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a724e3a48a54b8b6e7c4ae38cd3d07084508fa47c410c8757e9db9791421838"}, + {file = "matplotlib-3.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48e1e0859b54d5f2e29bb78ca179fd59b971c6ceb29977fb52735bfd280eb0f5"}, + {file = "matplotlib-3.4.3-cp38-cp38-win32.whl", hash = "sha256:01c9de93a2ca0d128c9064f23709362e7fefb34910c7c9e0b8ab0de8258d5eda"}, + {file = "matplotlib-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebfb01a65c3f5d53a8c2a8133fec2b5221281c053d944ae81ff5822a68266617"}, + {file = "matplotlib-3.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b53f336a4688cfce615887505d7e41fd79b3594bf21dd300531a4f5b4f746a"}, + {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:fcd6f1954943c0c192bfbebbac263f839d7055409f1173f80d8b11a224d236da"}, + {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6be8df61b1626e1a142c57e065405e869e9429b4a6dab4a324757d0dc4d42235"}, + {file = "matplotlib-3.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:41b6e307458988891fcdea2d8ecf84a8c92d53f84190aa32da65f9505546e684"}, + {file = "matplotlib-3.4.3-cp39-cp39-win32.whl", hash = "sha256:f72657f1596199dc1e4e7a10f52a4784ead8a711f4e5b59bea95bdb97cf0e4fd"}, + {file = "matplotlib-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:f15edcb0629a0801738925fe27070480f446fcaa15de65946ff946ad99a59a40"}, + {file = "matplotlib-3.4.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:556965514b259204637c360d213de28d43a1f4aed1eca15596ce83f768c5a56f"}, + {file = "matplotlib-3.4.3-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:54a026055d5f8614f184e588f6e29064019a0aa8448450214c0b60926d62d919"}, + {file = "matplotlib-3.4.3.tar.gz", hash = "sha256:fc4f526dfdb31c9bd6b8ca06bf9fab663ca12f3ec9cdf4496fb44bc680140318"}, +] +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"}, +] +mistune = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] +nbclient = [ + {file = "nbclient-0.5.4-py3-none-any.whl", hash = "sha256:95a300c6fbe73721736cf13972a46d8d666f78794b832866ed7197a504269e11"}, + {file = "nbclient-0.5.4.tar.gz", hash = "sha256:6c8ad36a28edad4562580847f9f1636fe5316a51a323ed85a24a4ad37d4aefce"}, +] +nbconvert = [ + {file = "nbconvert-6.2.0-py3-none-any.whl", hash = "sha256:b1b9dc4f1ff6cafae0e6d91f42fb9046fdc32e6beb6d7e2fa2cd7191ad535240"}, + {file = "nbconvert-6.2.0.tar.gz", hash = "sha256:16ceecd0afaa8fd26c245fa32e2c52066c02f13aa73387fffafd84750baea863"}, +] +nbformat = [ + {file = "nbformat-5.1.3-py3-none-any.whl", hash = "sha256:eb8447edd7127d043361bc17f2f5a807626bc8e878c7709a1c647abda28a9171"}, + {file = "nbformat-5.1.3.tar.gz", hash = "sha256:b516788ad70771c6250977c1374fcca6edebe6126fd2adb5a69aa5c2356fd1c8"}, +] +nest-asyncio = [ + {file = "nest_asyncio-1.5.1-py3-none-any.whl", hash = "sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c"}, + {file = "nest_asyncio-1.5.1.tar.gz", hash = "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"}, +] +notebook = [ + {file = "notebook-6.4.4-py3-none-any.whl", hash = "sha256:33488bdcc5cbef23c3cfa12cd51b0b5459a211945b5053d17405980611818149"}, + {file = "notebook-6.4.4.tar.gz", hash = "sha256:26b0095c568e307a310fd78818ad8ebade4f00462dada4c0e34cbad632b9085d"}, +] +numpy = [ + {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, + {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, + {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, + {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, + {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, + {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, + {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, + {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, + {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, +] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +pandas = [ + {file = "pandas-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68408a39a54ebadb9014ee5a4fae27b2fe524317bc80adf56c9ac59e8f8ea431"}, + {file = "pandas-1.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b16b1b920c4cb27fdd65a2c20258bcd9c794be491290660722bb0ea765054d"}, + {file = "pandas-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37d63e78e87eb3791da7be4100a65da0383670c2b59e493d9e73098d7a879226"}, + {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e2fb11f86f6253bb1df26e3aeab3bf2e000aaa32a953ec394571bec5dc6fd6"}, + {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7326b37de08d42dd3fff5b7ef7691d0fd0bf2428f4ba5a2bdc3b3247e9a52e4c"}, + {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2f29b4da6f6ae7c68f4b3708d9d9e59fa89b2f9e87c2b64ce055cbd39f729e"}, + {file = "pandas-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:3f5020613c1d8e304840c34aeb171377dc755521bf5e69804991030c2a48aec3"}, + {file = "pandas-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c399200631db9bd9335d013ec7fce4edb98651035c249d532945c78ad453f23a"}, + {file = "pandas-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a800df4e101b721e94d04c355e611863cc31887f24c0b019572e26518cbbcab6"}, + {file = "pandas-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3334a5a9eeaca953b9db1b2b165dcdc5180b5011f3bec3a57a3580c9c22eae68"}, + {file = "pandas-1.3.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fd2889d8116d7acef0709e4c82b8560a8b22b0f77471391d12c27596e90267"}, + {file = "pandas-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7557b39c8e86eb0543a17a002ac1ea0f38911c3c17095bc9350d0a65b32d801c"}, + {file = "pandas-1.3.3-cp38-cp38-win32.whl", hash = "sha256:629138b7cf81a2e55aa29ce7b04c1cece20485271d1f6c469c6a0c03857db6a4"}, + {file = "pandas-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:45649503e167d45360aa7c52f18d1591a6d5c70d2f3a26bc90a3297a30ce9a66"}, + {file = "pandas-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebbed7312547a924df0cbe133ff1250eeb94cdff3c09a794dc991c5621c8c735"}, + {file = "pandas-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f1b54d7efc9df05320b14a48fb18686f781aa66cc7b47bb62fabfc67a0985c"}, + {file = "pandas-1.3.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9bc59855598cb57f68fdabd4897d3ed2bc3a3b3bef7b868a0153c4cd03f3207"}, + {file = "pandas-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4def2ef2fb7fcd62f2aa51bacb817ee9029e5c8efe42fe527ba21f6a3ddf1a9f"}, + {file = "pandas-1.3.3-cp39-cp39-win32.whl", hash = "sha256:f7d84f321674c2f0f31887ee6d5755c54ca1ea5e144d6d54b3bbf566dd9ea0cc"}, + {file = "pandas-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:e574c2637c9d27f322e911650b36e858c885702c5996eda8a5a60e35e6648cf2"}, + {file = "pandas-1.3.3.tar.gz", hash = "sha256:272c8cb14aa9793eada6b1ebe81994616e647b5892a370c7135efb2924b701df"}, +] +pandocfilters = [ + {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, + {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, +] +parso = [ + {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, + {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pillow = [ + {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, + {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, + {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, + {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, + {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, + {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, + {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, + {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, + {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, + {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, + {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, + {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, + {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, + {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, +] +pip-chill = [ + {file = "pip-chill-1.0.1.tar.gz", hash = "sha256:5d76ac1b1ddf87efd291268cbeda94d3d4c053dc7de00ea0c7f03fe34ca557bb"}, + {file = "pip_chill-1.0.1-py2.py3-none-any.whl", hash = "sha256:90d9b86c7fc8b8a12fe89c497ed84f76d48a5f54f2b509f663837ffbb7c2bc46"}, +] +prometheus-client = [ + {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"}, + {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"}, + {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"}, +] +protobuf = [ + {file = "protobuf-3.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fa6d1049d5315566f55c04d0b50c0033415144f96a9d25c820dc542fe2bb7f45"}, + {file = "protobuf-3.18.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e2790c580070cff2921b93d562539ae027064340151c50db6aaf94c33048cd"}, + {file = "protobuf-3.18.1-cp36-cp36m-win32.whl", hash = "sha256:7e2f0677d68ecdd1cfda2abea65873f5bc7c3f5aae199404a3f5c1d1198c1a63"}, + {file = "protobuf-3.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6f714f5de9d40b3bec90ede4a688cce52f637ccdc5403afcda1f67598f4fdcd7"}, + {file = "protobuf-3.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a7be937c319146cc9f2626f0181e6809062c353e1fe449ecd0df374ba1036b2"}, + {file = "protobuf-3.18.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:10544fc7ace885a882623083c24da5b14148c77563acddc3c58d66f6153c09cd"}, + {file = "protobuf-3.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ee8b11e3eb2ed38f12137c3c132270a0b1dd509e317228ac47b67f21a583f1"}, + {file = "protobuf-3.18.1-cp37-cp37m-win32.whl", hash = "sha256:c492c217d3f69f4d2d5619571e52ab98538edbf53caf67e53ea92bd0a3b5670f"}, + {file = "protobuf-3.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3c1644f8a7f19b45c7a4c32278e2a55ae9e7e2f9e5f02d960a61f04a4890d3e6"}, + {file = "protobuf-3.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9ac691f7b24e4371dcd3980e4f5d6c840a2010da37986203053fee995786ec5"}, + {file = "protobuf-3.18.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:93bad12895d8b0ebc66b605c2ef1802311595f881aef032d9f13282b7550e6b2"}, + {file = "protobuf-3.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0851b5b89191e1976d34fa2e8eb8659829dfb45252053224cf9df857fb5f6a45"}, + {file = "protobuf-3.18.1-cp38-cp38-win32.whl", hash = "sha256:09d9268f6f9da81b7657adcf2fb397524c82f20cdf9e0db3ff4e7567977abd67"}, + {file = "protobuf-3.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6d927774c0ec746fed15a4faff5f44aad0b7a3421fadb6f3ae5ca1f2f8ae26e"}, + {file = "protobuf-3.18.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d19c9cb805fd2be1d59eee39e152367ee92a30167e77bd06c8819f8f0009a4c"}, + {file = "protobuf-3.18.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:387f621bf7295a331f8c8a6962d097ceddeb85356792888cfa6a5c6bfc6886a4"}, + {file = "protobuf-3.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1c5d3966c856f60a9d8d62f4455d70c31026422acdd5c228edf22b65b16c38"}, + {file = "protobuf-3.18.1-cp39-cp39-win32.whl", hash = "sha256:f20f803892f2135e8b96dc58c9a0c6a7ad8436794bf8784af229498d939b4c77"}, + {file = "protobuf-3.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:d76201380f41a2d83fb613a4683059d1fcafbe969518b3e409e279a8788fde2f"}, + {file = "protobuf-3.18.1-py2.py3-none-any.whl", hash = "sha256:61ca58e14033ca0dfa484a31d57237c1be3b6013454c7f53876a20fc88dd69b1"}, + {file = "protobuf-3.18.1.tar.gz", hash = "sha256:1c9bb40503751087300dd12ce2e90899d68628977905c76effc48e66d089391e"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pyarrow = [ + {file = "pyarrow-5.0.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:e9ec80f4a77057498cf4c5965389e42e7f6a618b6859e6dd615e57505c9167a6"}, + {file = "pyarrow-5.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1453c2411b5062ba6bf6832dbc4df211ad625f678c623a2ee177aee158f199b"}, + {file = "pyarrow-5.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9e04d3621b9f2f23898eed0d044203f66c156d880f02c5534a7f9947ebb1a4af"}, + {file = "pyarrow-5.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:64f30aa6b28b666a925d11c239344741850eb97c29d3aa0f7187918cf82494f7"}, + {file = "pyarrow-5.0.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:99c8b0f7e2ce2541dd4c0c0101d9944bb8e592ae3295fe7a2f290ab99222666d"}, + {file = "pyarrow-5.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:456a4488ae810a0569d1adf87dbc522bcc9a0e4a8d1809b934ca28c163d8edce"}, + {file = "pyarrow-5.0.0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c5493d2414d0d690a738aac8dd6d38518d1f9b870e52e24f89d8d7eb3afd4161"}, + {file = "pyarrow-5.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1832709281efefa4f199c639e9f429678286329860188e53beeda71750775923"}, + {file = "pyarrow-5.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b6387d2058d95fa48ccfedea810a768187affb62f4a3ef6595fa30bf9d1a65cf"}, + {file = "pyarrow-5.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bbe2e439bec2618c74a3bb259700c8a7353dc2ea0c5a62686b6cf04a50ab1e0d"}, + {file = "pyarrow-5.0.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5c0d1b68e67bb334a5af0cecdf9b6a702aaa4cc259c5cbb71b25bbed40fcedaf"}, + {file = "pyarrow-5.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6e937ce4a40ea0cc7896faff96adecadd4485beb53fbf510b46858e29b2e75ae"}, + {file = "pyarrow-5.0.0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:7560332e5846f0e7830b377c14c93624e24a17f91c98f0b25dafb0ca1ea6ba02"}, + {file = "pyarrow-5.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53e550dec60d1ab86cba3afa1719dc179a8bc9632a0e50d9fe91499cf0a7f2bc"}, + {file = "pyarrow-5.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2d26186ca9748a1fb89ae6c1fa04fb343a4279b53f118734ea8096f15d66c820"}, + {file = "pyarrow-5.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7c4edd2bacee3eea6c8c28bddb02347f9d41a55ec9692c71c6de6e47c62a7f0d"}, + {file = "pyarrow-5.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:601b0aabd6fb066429e706282934d4d8d38f53bdb8d82da9576be49f07eedf5c"}, + {file = "pyarrow-5.0.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ff21711f6ff3b0bc90abc8ca8169e676faeb2401ddc1a0bc1c7dc181708a3406"}, + {file = "pyarrow-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:ed135a99975380c27077f9d0e210aea8618ed9fadcec0e71f8a3190939557afe"}, + {file = "pyarrow-5.0.0-cp39-cp39-macosx_10_13_universal2.whl", hash = "sha256:6e1f0e4374061116f40e541408a8a170c170d0a070b788717e18165ebfdd2a54"}, + {file = "pyarrow-5.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:4341ac0f552dc04c450751e049976940c7f4f8f2dae03685cc465ebe0a61e231"}, + {file = "pyarrow-5.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3fc856f107ca2fb3c9391d7ea33bbb33f3a1c2b4a0e2b41f7525c626214cc03"}, + {file = "pyarrow-5.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:357605665fbefb573d40939b13a684c2490b6ed1ab4a5de8dd246db4ab02e5a4"}, + {file = "pyarrow-5.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f4db312e9ba80e730cefcae0a05b63ea5befc7634c28df56682b628ad8e1c25c"}, + {file = "pyarrow-5.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1d9485741e497ccc516cb0a0c8f56e22be55aea815be185c3f9a681323b0e614"}, + {file = "pyarrow-5.0.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:b3115df938b8d7a7372911a3cb3904196194bcea8bb48911b4b3eafee3ab8d90"}, + {file = "pyarrow-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d8adda1892ef4553c4804af7f67cce484f4d6371564e2d8374b8e2bc85293e2"}, + {file = "pyarrow-5.0.0.tar.gz", hash = "sha256:24e64ea33eed07441cc0e80c949e3a1b48211a1add8953268391d250f4d39922"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pydantic = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] +pydeck = [ + {file = "pydeck-0.7.0-py2.py3-none-any.whl", hash = "sha256:8db699ff8f5226e6b26d04aa01f9a32e986427219c176a216c8156fd5a37bfc6"}, + {file = "pydeck-0.7.0.tar.gz", hash = "sha256:5eef4d510b6b1f50d5e9c01b3121cd150695526da292a3a409d22fd03e7e20fe"}, +] +pygments = [ + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyrsistent = [ + {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, + {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, + {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [ + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, +] +pywaffle = [ + {file = "pywaffle-0.6.3-py2.py3-none-any.whl", hash = "sha256:30e122fb516d0731659a4ac8d0eaae1b858bb1fdcee76f224cc30c101496c6c1"}, + {file = "pywaffle-0.6.3.tar.gz", hash = "sha256:f2c97b74c9629a7a4988ec94c63849ac4b8dd06633e2c2804a048f8c73664876"}, +] +pywin32 = [ + {file = "pywin32-302-cp310-cp310-win32.whl", hash = "sha256:251b7a9367355ccd1a4cd69cd8dd24bd57b29ad83edb2957cfa30f7ed9941efa"}, + {file = "pywin32-302-cp310-cp310-win_amd64.whl", hash = "sha256:79cf7e6ddaaf1cd47a9e50cc74b5d770801a9db6594464137b1b86aa91edafcc"}, + {file = "pywin32-302-cp36-cp36m-win32.whl", hash = "sha256:fe21c2fb332d03dac29de070f191bdbf14095167f8f2165fdc57db59b1ecc006"}, + {file = "pywin32-302-cp36-cp36m-win_amd64.whl", hash = "sha256:d3761ab4e8c5c2dbc156e2c9ccf38dd51f936dc77e58deb940ffbc4b82a30528"}, + {file = "pywin32-302-cp37-cp37m-win32.whl", hash = "sha256:48dd4e348f1ee9538dd4440bf201ea8c110ea6d9f3a5010d79452e9fa80480d9"}, + {file = "pywin32-302-cp37-cp37m-win_amd64.whl", hash = "sha256:496df89f10c054c9285cc99f9d509e243f4e14ec8dfc6d78c9f0bf147a893ab1"}, + {file = "pywin32-302-cp38-cp38-win32.whl", hash = "sha256:e372e477d938a49266136bff78279ed14445e00718b6c75543334351bf535259"}, + {file = "pywin32-302-cp38-cp38-win_amd64.whl", hash = "sha256:543552e66936378bd2d673c5a0a3d9903dba0b0a87235ef0c584f058ceef5872"}, + {file = "pywin32-302-cp39-cp39-win32.whl", hash = "sha256:2393c1a40dc4497fd6161b76801b8acd727c5610167762b7c3e9fd058ef4a6ab"}, + {file = "pywin32-302-cp39-cp39-win_amd64.whl", hash = "sha256:af5aea18167a31efcacc9f98a2ca932c6b6a6d91ebe31f007509e293dea12580"}, +] +pywinpty = [ + {file = "pywinpty-1.1.4-cp36-none-win_amd64.whl", hash = "sha256:fb975976ad92be44801de95fdf2b0366747767cb0528478553aff85dd63ebb09"}, + {file = "pywinpty-1.1.4-cp37-none-win_amd64.whl", hash = "sha256:5d25b30a2f87105778bc2f57cb1271f58aaa25568921ef042faf001b3b0a7307"}, + {file = "pywinpty-1.1.4-cp38-none-win_amd64.whl", hash = "sha256:c5c3550100689632f6663f39865ef8716835dab1838a9eb9b472644af92673f8"}, + {file = "pywinpty-1.1.4-cp39-none-win_amd64.whl", hash = "sha256:ad60a336d92ac38e2159320db6d5999c4c2726a141c3ed3f9694021feb6a234e"}, + {file = "pywinpty-1.1.4.tar.gz", hash = "sha256:cc700c9d5a9fcebf677ac93a4943ca9a24db6e2f11a5f0e7e8e226184c5036f7"}, +] +pyzmq = [ + {file = "pyzmq-22.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:6b217b8f9dfb6628f74b94bdaf9f7408708cb02167d644edca33f38746ca12dd"}, + {file = "pyzmq-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2841997a0d85b998cbafecb4183caf51fd19c4357075dfd33eb7efea57e4c149"}, + {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, + {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, + {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, + {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, + {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, + {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, + {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, + {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, + {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, + {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, + {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, + {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, + {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, + {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, + {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, + {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, + {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, + {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, + {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, + {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, + {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, + {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, + {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, + {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, + {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f43b4a2e6218371dd4f41e547bd919ceeb6ebf4abf31a7a0669cd11cd91ea973"}, + {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, + {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, + {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, + {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, + {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:80e043a89c6cadefd3a0712f8a1322038e819ebe9dbac7eca3bce1721bcb63bf"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1621e7a2af72cced1f6ec8ca8ca91d0f76ac236ab2e8828ac8fe909512d566cb"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d6157793719de168b199194f6b6173f0ccd3bf3499e6870fac17086072e39115"}, + {file = "pyzmq-22.3.0.tar.gz", hash = "sha256:8eddc033e716f8c91c6a2112f0a8ebc5e00532b4a6ae1eb0ccc48e027f9c671c"}, +] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +send2trash = [ + {file = "Send2Trash-1.8.0-py3-none-any.whl", hash = "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08"}, + {file = "Send2Trash-1.8.0.tar.gz", hash = "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [ + {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, + {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +starlette = [ + {file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"}, + {file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"}, +] +streamlit = [ + {file = "streamlit-1.0.0-py2.py3-none-any.whl", hash = "sha256:1cf88c7dc278d70136138bf3ee3e0dd8a185bc5a4375479c1ad871865b02aa12"}, + {file = "streamlit-1.0.0.tar.gz", hash = "sha256:f94f8f588e70471377a7ecf8d994efc045cc3c10c00b74220d90032517294a91"}, +] +streamlit-autorefresh = [ + {file = "streamlit-autorefresh-0.0.1.tar.gz", hash = "sha256:1f25e3c7de26fa51d8bc2934cc05ea24b853d66148f110bcb1e4fc0e9279ec00"}, + {file = "streamlit_autorefresh-0.0.1-py3-none-any.whl", hash = "sha256:c3a1a8a209d575cca17648cfb125b423bf001476e4d718b1fcc30c2d36c94c64"}, +] +terminado = [ + {file = "terminado-0.12.1-py3-none-any.whl", hash = "sha256:09fdde344324a1c9c6e610ee4ca165c4bb7f5bbf982fceeeb38998a988ef8452"}, + {file = "terminado-0.12.1.tar.gz", hash = "sha256:b20fd93cc57c1678c799799d117874367cc07a3d2d55be95205b1a88fa08393f"}, +] +testpath = [ + {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"}, + {file = "testpath-0.5.0.tar.gz", hash = "sha256:1acf7a0bcd3004ae8357409fc33751e16d37ccc650921da1094a86581ad1e417"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +toolz = [ + {file = "toolz-0.11.1-py3-none-any.whl", hash = "sha256:1bc473acbf1a1db4e72a1ce587be347450e8f08324908b8a266b486f408f04d5"}, + {file = "toolz-0.11.1.tar.gz", hash = "sha256:c7a47921f07822fe534fb1c01c9931ab335a4390c782bd28c6bcc7c2f71f3fbf"}, +] +tornado = [ + {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, + {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, + {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, + {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, + {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, + {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, + {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, + {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, + {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, + {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, + {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, + {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, + {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, + {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, + {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, + {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, + {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, + {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, +] +traitlets = [ + {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, + {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, +] +tzdata = [ + {file = "tzdata-2021.2.post0-py2.py3-none-any.whl", hash = "sha256:a843aabf67dea3dc6bbbc8853e1aee6563e74bcfe920e11a571a389155db1401"}, + {file = "tzdata-2021.2.post0.tar.gz", hash = "sha256:99d30a01967bb8d7868c03dc924862b1ae8a0e649a322a1107bacc1723c430b9"}, +] +tzlocal = [ + {file = "tzlocal-3.0-py3-none-any.whl", hash = "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559"}, + {file = "tzlocal-3.0.tar.gz", hash = "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12"}, +] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] +uvicorn = [ + {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, + {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, +] +validators = [ + {file = "validators-0.18.2-py3-none-any.whl", hash = "sha256:0143dcca8a386498edaf5780cbd5960da1a4c85e0719f3ee5c9b41249c4fefbd"}, + {file = "validators-0.18.2.tar.gz", hash = "sha256:37cd9a9213278538ad09b5b9f9134266e7c226ab1fede1d500e29e0a8fbb9ea6"}, +] +watchdog = [ + {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, + {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, + {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, + {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, + {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, + {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, + {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, + {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, + {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, + {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, + {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, + {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, + {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, + {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, + {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, + {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] +widgetsnbextension = [ + {file = "widgetsnbextension-3.5.1-py2.py3-none-any.whl", hash = "sha256:bd314f8ceb488571a5ffea6cc5b9fc6cba0adaf88a9d2386b93a489751938bcd"}, + {file = "widgetsnbextension-3.5.1.tar.gz", hash = "sha256:079f87d87270bce047512400efd70238820751a11d2d8cb137a5a5bdbaf255c7"}, +] diff --git a/dashboard/pyproject.toml b/dashboard/pyproject.toml new file mode 100644 index 00000000..49a5bff1 --- /dev/null +++ b/dashboard/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "dashboard" +version = "0.1.0" +description = "Dasboard app for the Cocktailmaker" +authors = ["Andre "] + +[tool.poetry.dependencies] +python = "^3.8" +fastapi = "^0.70.0" +uvicorn = "^0.15.0" +streamlit = "^1.0.0" +pip-chill = "^1.0.1" +matplotlib = "^3.4.3" +pywaffle = "^0.6.3" +streamlit-autorefresh = "^0.0.1" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/dashboard/storage/.gitkeep b/dashboard/storage/.gitkeep new file mode 100644 index 00000000..e69de29b From f50b238318c016feca8d23f0bcadceaea6f641c3 Mon Sep 17 00:00:00 2001 From: Andre Date: Wed, 13 Oct 2021 19:58:51 +0200 Subject: [PATCH 14/65] Refactored sending, added team post --- src/service_handler.py | 85 ++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/src/service_handler.py b/src/service_handler.py index 257b5279..1b85d9c0 100644 --- a/src/service_handler.py +++ b/src/service_handler.py @@ -13,51 +13,78 @@ def __init__(self): self.logger = LoggerHandler("microservice", "service_logs") self.headers = {"content-type": "application/json"} - def log_connection_error(self, func: str): - self.logger.log_event("ERROR", f"Could not connect to the microservice endpoint: '{func}'") + def log_connection_error(self, endpoint: str): + self.logger.log_event("ERROR", f"Could not connect to the microservice endpoint: '{endpoint}'") def post_cocktail_to_hook(self, cocktailname: str, cocktail_volume: int) -> Dict: + """Post the given cocktail data to the microservice handling internet traffic to send to defined webhook""" if not self.USE_MICROSERVICE: - return return_service_disabled() + return service_disabled() # calculate volume in litre payload = json.dumps({"cocktailname": cocktailname, "volume": cocktail_volume / 1000}) - endpoint = "/hookhandler/cocktail" - full_url = f"{self.base_url}{endpoint}" - ret_data = {} - try: - req = requests.post(full_url, data=payload, headers=self.headers) - message = str(req.text).replace("\n", "") - ret_data = { - "status": req.status_code, - "message": message, - } - self.logger.log_event("INFO", f"Posted cocktail to {full_url} | {req.status_code}: {message}") - except requests.exceptions.ConnectionError: - self.log_connection_error(full_url) - return ret_data + endpoint = f"{self.base_url}/hookhandler/cocktail" + return self.try_to_send(endpoint, payload=payload, post_type="cocktail") - def send_mail(self, file_name, binary_file): + def send_mail(self, file_name: str, binary_file) -> Dict: + """Post the given file to the microservice handling internet traffic to send as mail""" if not self.USE_MICROSERVICE: - return return_service_disabled() - endpoint = "/email" - full_url = f"{self.base_url}{endpoint}" - ret_data = {} + return service_disabled() + endpoint = f"{self.base_url}/email" files = {"upload_file": (file_name, binary_file,)} + return self.try_to_send(endpoint, post_type="file", files=files) + + def post_team_data(self, team_name: str) -> Dict: + """Post the given team name to the team api if activated""" + if not self.USE_TEAMS: + return team_disabled() + payload = json.dumps({"team": team_name}) + endpoint = f"{self.TEAM_API_URL}/cocktail" + return self.try_to_send(endpoint, payload=payload, post_type="teamdata") + + def try_to_send(self, endpoint: str, payload: str = None, post_type: str = "", files: dict = None) -> Dict: + """Try to send the data to the given endpoint. + Logs the action, catches and logs if there is no connection. + Raises an exception if there is no data to send. + + Args: + endpoint (str): url to send + payload (str, optional): JSON data for payload. Defaults to None. + post_type (str, optional): Addional info for logger what was posted. Defaults to "". + files (dict, optional): dict with key 'upload_file' + filename and binary data as tuple. Defaults to None. + + Raises: + Exception: There is no data to send. This shouldn't be happening if used correctly. + + Returns: + Dict: Statuscode and message, or empty if cannot reach service + """ try: - req = requests.post(full_url, files=files) + if payload is not None: + req = requests.post(endpoint, data=payload, headers=self.headers) + elif files is not None: + req = requests.post(endpoint, files=files) + else: + raise Exception('Neither payload nor files given!') message = str(req.text).replace("\n", "") - ret_data = { + self.logger.log_event("INFO", f"Posted {post_type} to {endpoint} | {req.status_code}: {message}") + return { "status": req.status_code, "message": message, } - self.logger.log_event("INFO", f"Posted file to {full_url} | {req.status_code}: {message}") except requests.exceptions.ConnectionError: - self.log_connection_error(full_url) - return ret_data + self.log_connection_error(endpoint) + return {} + + +def service_disabled(): + return { + "status": 503, + "message": "Microservice disabled", + } -def return_service_disabled(): +def team_disabled(): return { "status": 503, - "message": "microservice disabled", + "message": "Teams disabled", } From cd8f91e7b02c4e47283fd8d897d279f4464d7383 Mon Sep 17 00:00:00 2001 From: Andre Date: Wed, 13 Oct 2021 23:35:59 +0200 Subject: [PATCH 15/65] Added needed folders --- dashboard/backend/storage/.gitkeep | 0 dashboard/frontend/storage/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 dashboard/backend/storage/.gitkeep create mode 100644 dashboard/frontend/storage/.gitkeep diff --git a/dashboard/backend/storage/.gitkeep b/dashboard/backend/storage/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/frontend/storage/.gitkeep b/dashboard/frontend/storage/.gitkeep new file mode 100644 index 00000000..e69de29b From 03f818cee1f7c912bc68db224fbb7473a314cc19 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 17:40:48 +0200 Subject: [PATCH 16/65] Adjusted restart policy --- dashboard/docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index c74bf782..546e57c0 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -2,6 +2,7 @@ version: '3' services: frontend: + restart: always build: frontend ports: - 8501:8501 @@ -10,6 +11,7 @@ services: volumes: - ./storage:/app/storage backend: + restart: always build: backend ports: - 8080:8080 From 9f643dfef6654b815c3a9c2cf91f11993d920320 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 17:41:25 +0200 Subject: [PATCH 17/65] Added API for dashboard --- dashboard/backend/main.py | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 dashboard/backend/main.py diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py new file mode 100644 index 00000000..bd7953cf --- /dev/null +++ b/dashboard/backend/main.py @@ -0,0 +1,55 @@ +import os +import datetime +from pathlib import Path +import sqlite3 +import uvicorn +from fastapi import FastAPI +from pydantic import BaseModel + +DATABASE_NAME = "team" +DIRPATH = os.path.dirname(__file__) +database_path = os.path.join(DIRPATH, "storage", f"{DATABASE_NAME}.db") + +app = FastAPI() + + +class Teaminfo(BaseModel): + team: str + volume: int + + +@app.get("/") +def home(): + return {"message": "Welcome to dashboard api"} + + +@app.post("/cocktail") +async def enter_cocktail_for_team(team: Teaminfo): + conn = sqlite3.connect(database_path) + cursor = conn.cursor() + entry_datetime = datetime.datetime.now().replace(microsecond=0) + SQL = "INSERT INTO TEAM(Date, Team, Volume) VALUES(?,?,?)" + cursor.execute(SQL, (entry_datetime, team.team, team.volume,)) + conn.commit() + conn.close() + return {"message": "Team entry was Successfull", "team": team.team, "volume": team.volume} + + +def create_tables(): + conn = sqlite3.connect(database_path) + cursor = conn.cursor() + cursor.execute( + """CREATE TABLE IF NOT EXISTS + Team(Date DATETIME NOT NULL, + Team TEXT NOT NULL, + Volume INTEGER NOT NULL);""" + ) + conn.commit() + conn.close() + + +if __name__ == "__main__": + if not Path(database_path).exists(): + print("creating Database") + create_tables() + uvicorn.run("main:app", host="0.0.0.0", port=8080) From 9748b683707e1dc52e00b1ba48d0a042d28e9824 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 17:41:36 +0200 Subject: [PATCH 18/65] Added frontend for dashboard --- dashboard/frontend/main.py | 116 +++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 dashboard/frontend/main.py diff --git a/dashboard/frontend/main.py b/dashboard/frontend/main.py new file mode 100644 index 00000000..68a4ffd3 --- /dev/null +++ b/dashboard/frontend/main.py @@ -0,0 +1,116 @@ +import math +import os +import sqlite3 +import streamlit as st +from streamlit_autorefresh import st_autorefresh +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib as mpl +from pywaffle import Waffle +import warnings + +st.set_page_config( + page_title="Cocktail Dashboard", + page_icon="🍸", + layout="wide", + initial_sidebar_state="collapsed", +) +warnings.filterwarnings("ignore", 'Starting a Matplotlib GUI outside of the main thread will likely fail.') +mpl.rcParams.update({'text.color': "white", 'axes.labelcolor': "white"}) + +DATABASE_NAME = "team" +DIRPATH = os.path.dirname(__file__) +database_path = os.path.join(DIRPATH, "storage", f"{DATABASE_NAME}.db") + + +def get_leaderboard(hourrange=None, limit=2, count=True): + addition = "" + if hourrange is not None: + addition = f" WHERE Date >= datetime('now','-{hourrange} hours')" + agg = "count(*)" if count else "sum(Volume)" + conn = sqlite3.connect(database_path) + SQL = f"SELECT Team, {agg} as amount FROM Team{addition} GROUP BY Team ORDER BY {agg} DESC LIMIT ?" + board = pd.read_sql(SQL, conn, params=(limit,)) + board.reset_index(drop=True, inplace=True) + conn.close() + return board + + +def sort_dict_items(to_sort: dict): + dictionary_items = to_sort.items() + sorted_items = sorted(dictionary_items) + return {x[0]: x[1] for x in sorted_items} + + +def extract_data(sort: bool, df: pd.DataFrame): + if df.empty or sum(df.amount.to_list()) < 3: + waffle_data = {"Cocktails trinken zum starten ...": 3} + else: + waffle_data = {f"{x} ({y})": y for x, y in zip(df.Team.to_list(), df.amount.to_list())} + if sort: + waffle_data = sort_dict_items(waffle_data) + return waffle_data + + +def generate_dimensions(total: float, count=True): + proportion = 3 + threshold_upper = 1.2 + threshold_lower = 0.77 + one_row_until = 9 + # use fixed grid for non count variables + if not count: + return {"rows": 10, "columns": 25} + # don't split until given dimension + if total < one_row_until: + return {"rows": 1} + row = max(math.floor(math.sqrt(total / proportion)), 1) + # calculate current proportion, add one if exceeds th + column = math.ceil(total / row) + real_prop = column / row + if real_prop >= proportion * threshold_upper: + row += 1 + # calculates adjusted proportion, rolls back if its too extreme + column = math.ceil(total / row) + adjusted_prop = column / row + if adjusted_prop <= proportion * threshold_lower: + row -= 1 + return {"rows": row} + + +def generate_figure(title: str, hourrange: int = None, limit=5, sort=False, count=True): + df = get_leaderboard(hourrange, limit, count) + waffle_data = extract_data(sort, df) + dims = generate_dimensions(sum(df.amount.to_list()), count) + fig = plt.figure( + FigureClass=Waffle, + **dims, + values=waffle_data, + title={ + 'label': title, + 'fontdict': { + 'fontsize': 20 + } + }, + facecolor=(0.054, 0.066, 0.090, 1), + legend={'loc': 'upper center', 'bbox_to_anchor': (0.5, 0.0), 'ncol': 2, 'framealpha': 0} + ) + return fig + + +st_autorefresh(interval=15000, key="autorefresh") +st.markdown(""" """, unsafe_allow_html=True) + +st.sidebar.header("Zeit aussuchen") +selected_display = st.sidebar.radio("", ("Heute", "All time")) +st.sidebar.header("Aggregation aussuchen") +selected_type = st.sidebar.radio("", ("Anzahl", "Volumen")) +use_count = selected_type == "Anzahl" + +if selected_display == "Heute": + st.pyplot(generate_figure(title=f"Leaderboard ({selected_type}, Heute)", hourrange=24, sort=True, count=use_count)) +else: + st.pyplot(generate_figure(title=f"Leaderboard ({selected_type}, All time)", limit=20, count=use_count)) From 5a32a399668803bef4dd03a2fe8bc5c79006e278 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 17:59:18 +0200 Subject: [PATCH 19/65] Fix for Permission error pip --- dashboard/docker-compose.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index 546e57c0..2d817bbe 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -3,6 +3,9 @@ version: '3' services: frontend: restart: always + cap_add: + - NET_ADMIN + - NET_RAW build: frontend ports: - 8501:8501 From 0b630451e5ccdf6923ca9afa721b0dc8f5c66b9a Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 18:06:00 +0200 Subject: [PATCH 20/65] Second fix --- dashboard/docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index 2d817bbe..018b7145 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -3,6 +3,7 @@ version: '3' services: frontend: restart: always + privileged: true cap_add: - NET_ADMIN - NET_RAW From 041f20b6bb5bb22e82e17fc6ae2fef8531f6c2b2 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 18:10:40 +0200 Subject: [PATCH 21/65] Fix 3 --- dashboard/docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index 018b7145..8a8a6641 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -4,6 +4,7 @@ services: frontend: restart: always privileged: true + user: root cap_add: - NET_ADMIN - NET_RAW From 7f0e811995acbe2d5e34aeea9b0998d9a6c73f42 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 18:15:33 +0200 Subject: [PATCH 22/65] Trying buster instead slim --- dashboard/docker-compose.yaml | 10 +++++----- dashboard/frontend/Dockerfile | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index 8a8a6641..fc6be702 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -3,11 +3,11 @@ version: '3' services: frontend: restart: always - privileged: true - user: root - cap_add: - - NET_ADMIN - - NET_RAW + # privileged: true + # user: root + # cap_add: + # - NET_ADMIN + # - NET_RAW build: frontend ports: - 8501:8501 diff --git a/dashboard/frontend/Dockerfile b/dashboard/frontend/Dockerfile index 1e59dd40..63b58c4b 100644 --- a/dashboard/frontend/Dockerfile +++ b/dashboard/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.8-buster WORKDIR /app From 931bed68d4334060cedbc71bdeaf81460bc2a38b Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 20:01:47 +0200 Subject: [PATCH 23/65] Adjusted image an name --- dashboard/docker-compose.yaml | 9 ++++----- dashboard/frontend/Dockerfile | 2 +- docker-compose.yml | 2 ++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index fc6be702..a954a6e0 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -2,13 +2,10 @@ version: '3' services: frontend: + name: cocktail-dashboard-frontend restart: always - # privileged: true - # user: root - # cap_add: - # - NET_ADMIN - # - NET_RAW build: frontend + image: andrewo92/coktaildashboard-frontend:latest ports: - 8501:8501 depends_on: @@ -16,8 +13,10 @@ services: volumes: - ./storage:/app/storage backend: + name: cocktail-dashboard-frontend restart: always build: backend + image: andrewo92/coktaildashboard-backend:latest ports: - 8080:8080 volumes: diff --git a/dashboard/frontend/Dockerfile b/dashboard/frontend/Dockerfile index 63b58c4b..1e59dd40 100644 --- a/dashboard/frontend/Dockerfile +++ b/dashboard/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-buster +FROM python:3.8-slim WORKDIR /app diff --git a/docker-compose.yml b/docker-compose.yml index 074358c3..192e5589 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,10 @@ version: '3.8' services: api-service: + name: cocktail-microservice restart: always build: ./microservice/ + image: andrewo92/coktail-microservice:latest volumes: - ./microservice/:/usr/src/app/ ports: From 4cadcb8be69ae0a7180a2535983ef53ad0db4479 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 20:03:15 +0200 Subject: [PATCH 24/65] Fixed container name param --- dashboard/docker-compose.yaml | 4 ++-- docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index a954a6e0..a504aaeb 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3' services: frontend: - name: cocktail-dashboard-frontend + container_name: cocktail-dashboard-frontend restart: always build: frontend image: andrewo92/coktaildashboard-frontend:latest @@ -13,7 +13,7 @@ services: volumes: - ./storage:/app/storage backend: - name: cocktail-dashboard-frontend + container_name: cocktail-dashboard-frontend restart: always build: backend image: andrewo92/coktaildashboard-backend:latest diff --git a/docker-compose.yml b/docker-compose.yml index 192e5589..dcb53572 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.8' services: api-service: - name: cocktail-microservice + container_name: cocktail-microservice restart: always build: ./microservice/ image: andrewo92/coktail-microservice:latest From 4f1d1190947a5527b17ae2296af3894c26ea34e8 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 Oct 2021 20:15:24 +0200 Subject: [PATCH 25/65] Fixed identical names --- dashboard/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index a504aaeb..9e40801d 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -13,7 +13,7 @@ services: volumes: - ./storage:/app/storage backend: - container_name: cocktail-dashboard-frontend + container_name: cocktail-dashboard-backend restart: always build: backend image: andrewo92/coktaildashboard-backend:latest From 6591085d3914cf0d9b6e0d8242be544159d43136 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 15 Oct 2021 08:25:06 +0200 Subject: [PATCH 26/65] Adjustments to work on armv7 --- dashboard/backend/Dockerfile | 1 + dashboard/frontend/Dockerfile | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dashboard/backend/Dockerfile b/dashboard/backend/Dockerfile index e85ead33..01ab9d82 100644 --- a/dashboard/backend/Dockerfile +++ b/dashboard/backend/Dockerfile @@ -3,6 +3,7 @@ FROM python:3.8-slim WORKDIR /app COPY requirements.txt . +RUN python -m pip install --upgrade pip RUN pip install -r requirements.txt COPY . . diff --git a/dashboard/frontend/Dockerfile b/dashboard/frontend/Dockerfile index 1e59dd40..f2978fe8 100644 --- a/dashboard/frontend/Dockerfile +++ b/dashboard/frontend/Dockerfile @@ -1,8 +1,10 @@ -FROM python:3.8-slim +FROM python:3.9-buster WORKDIR /app COPY requirements.txt . + +RUN python -m pip install --upgrade pip RUN pip install -r requirements.txt COPY . . From 78719a56f32bd7ed5e18f3468a2670a8a84d4828 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 15 Oct 2021 08:25:26 +0200 Subject: [PATCH 27/65] Removed version to match on armv7 --- dashboard/frontend/requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dashboard/frontend/requirements.txt b/dashboard/frontend/requirements.txt index 48302bbd..40ffec93 100644 --- a/dashboard/frontend/requirements.txt +++ b/dashboard/frontend/requirements.txt @@ -1,4 +1,4 @@ -streamlit==1.0.0 -streamlit-autorefresh == 0.0.1 -matplotlib == 3.4.3 -pywaffle == 0.6.3 \ No newline at end of file +streamlit +streamlit-autorefresh +matplotlib +pywaffle \ No newline at end of file From 253d82563e27c5e5411ba046d73e6b0847e8e72b Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 15 Oct 2021 08:30:40 +0200 Subject: [PATCH 28/65] Rolling Back pip upgrade --- dashboard/backend/Dockerfile | 1 - dashboard/frontend/Dockerfile | 2 -- 2 files changed, 3 deletions(-) diff --git a/dashboard/backend/Dockerfile b/dashboard/backend/Dockerfile index 01ab9d82..e85ead33 100644 --- a/dashboard/backend/Dockerfile +++ b/dashboard/backend/Dockerfile @@ -3,7 +3,6 @@ FROM python:3.8-slim WORKDIR /app COPY requirements.txt . -RUN python -m pip install --upgrade pip RUN pip install -r requirements.txt COPY . . diff --git a/dashboard/frontend/Dockerfile b/dashboard/frontend/Dockerfile index f2978fe8..b2d3c60f 100644 --- a/dashboard/frontend/Dockerfile +++ b/dashboard/frontend/Dockerfile @@ -3,8 +3,6 @@ FROM python:3.9-buster WORKDIR /app COPY requirements.txt . - -RUN python -m pip install --upgrade pip RUN pip install -r requirements.txt COPY . . From 5ba92ed986a66bebada0c49c437e2542d26ce54a Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Sat, 16 Oct 2021 15:40:58 +0200 Subject: [PATCH 29/65] Changed default compose only use backend --- dashboard/docker-compose.both.yaml | 21 +++++++++++++++++++++ dashboard/docker-compose.yaml | 12 ------------ 2 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 dashboard/docker-compose.both.yaml diff --git a/dashboard/docker-compose.both.yaml b/dashboard/docker-compose.both.yaml new file mode 100644 index 00000000..85839547 --- /dev/null +++ b/dashboard/docker-compose.both.yaml @@ -0,0 +1,21 @@ +version: '3' + +services: + frontend: + container_name: cocktail-dashboard-frontend + restart: always + build: frontend + ports: + - 8501:8501 + depends_on: + - backend + volumes: + - ./storage:/app/storage + backend: + container_name: cocktail-dashboard-backend + restart: always + build: backend + ports: + - 8080:8080 + volumes: + - ./storage:/app/storage diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index 9e40801d..8061d14d 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -1,22 +1,10 @@ version: '3' services: - frontend: - container_name: cocktail-dashboard-frontend - restart: always - build: frontend - image: andrewo92/coktaildashboard-frontend:latest - ports: - - 8501:8501 - depends_on: - - backend - volumes: - - ./storage:/app/storage backend: container_name: cocktail-dashboard-backend restart: always build: backend - image: andrewo92/coktaildashboard-backend:latest ports: - 8080:8080 volumes: From 64bd7d49381de050c291cb45c4d436ea524fb931 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Sat, 16 Oct 2021 15:41:31 +0200 Subject: [PATCH 30/65] Added get leaderboard endpoint --- dashboard/backend/main.py | 24 ++++++++++++++++++++++++ dashboard/backend/requirements.txt | 1 + 2 files changed, 25 insertions(+) diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py index bd7953cf..5a78d6fe 100644 --- a/dashboard/backend/main.py +++ b/dashboard/backend/main.py @@ -2,6 +2,8 @@ import datetime from pathlib import Path import sqlite3 +from typing import Optional +import pandas as pd import uvicorn from fastapi import FastAPI from pydantic import BaseModel @@ -35,6 +37,28 @@ async def enter_cocktail_for_team(team: Teaminfo): return {"message": "Team entry was Successfull", "team": team.team, "volume": team.volume} +def get_leaderboard(hourrange=None, limit=2, count=True): + addition = "" + if hourrange is not None: + addition = f" WHERE Date >= datetime('now','-{hourrange} hours')" + agg = "count(*)" if count else "sum(Volume)" + conn = sqlite3.connect(database_path) + SQL = f"SELECT Team, {agg} as amount FROM Team{addition} GROUP BY Team ORDER BY {agg} DESC LIMIT ?" + df = pd.read_sql(SQL, conn, params=(limit,)) + conn.close() + return_data = {x: y for x, y in zip(df.Team.to_list(), df.amount.to_list())} + return return_data + + +@app.get("/leaderboard") +def leaderboard( + hourrange: Optional[int] = 24, + limit: Optional[int] = 5, + count: Optional[bool] = True +): + return get_leaderboard(hourrange, limit, count) + + def create_tables(): conn = sqlite3.connect(database_path) cursor = conn.cursor() diff --git a/dashboard/backend/requirements.txt b/dashboard/backend/requirements.txt index 8353fce9..63873ac6 100644 --- a/dashboard/backend/requirements.txt +++ b/dashboard/backend/requirements.txt @@ -1,2 +1,3 @@ fastapi==0.70.0 uvicorn==0.15.0 +pandas \ No newline at end of file From f2f0355740492cfa0c91b87e91a23baa8db7a2e2 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Sat, 16 Oct 2021 15:45:50 +0200 Subject: [PATCH 31/65] made vars case conform --- dashboard/backend/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py index 5a78d6fe..5a9fb446 100644 --- a/dashboard/backend/main.py +++ b/dashboard/backend/main.py @@ -30,8 +30,8 @@ async def enter_cocktail_for_team(team: Teaminfo): conn = sqlite3.connect(database_path) cursor = conn.cursor() entry_datetime = datetime.datetime.now().replace(microsecond=0) - SQL = "INSERT INTO TEAM(Date, Team, Volume) VALUES(?,?,?)" - cursor.execute(SQL, (entry_datetime, team.team, team.volume,)) + sql = "INSERT INTO TEAM(Date, Team, Volume) VALUES(?,?,?)" + cursor.execute(sql, (entry_datetime, team.team, team.volume,)) conn.commit() conn.close() return {"message": "Team entry was Successfull", "team": team.team, "volume": team.volume} @@ -43,8 +43,8 @@ def get_leaderboard(hourrange=None, limit=2, count=True): addition = f" WHERE Date >= datetime('now','-{hourrange} hours')" agg = "count(*)" if count else "sum(Volume)" conn = sqlite3.connect(database_path) - SQL = f"SELECT Team, {agg} as amount FROM Team{addition} GROUP BY Team ORDER BY {agg} DESC LIMIT ?" - df = pd.read_sql(SQL, conn, params=(limit,)) + sql = f"SELECT Team, {agg} as amount FROM Team{addition} GROUP BY Team ORDER BY {agg} DESC LIMIT ?" + df = pd.read_sql(sql, conn, params=(limit,)) conn.close() return_data = {x: y for x, y in zip(df.Team.to_list(), df.amount.to_list())} return return_data From e7faf6e28ba19ceb6197f9013bf865a6cb660f63 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Sat, 16 Oct 2021 15:46:03 +0200 Subject: [PATCH 32/65] First draft for qt-display --- dashboard/qt-app/main.py | 11 +++ dashboard/qt-app/mainwindow.py | 113 +++++++++++++++++++++ dashboard/qt-app/mainwindow.ui | 135 ++++++++++++++++++++++++++ dashboard/qt-app/setup_leaderboard.py | 30 ++++++ dashboard/qt-app/waffle.py | 69 +++++++++++++ 5 files changed, 358 insertions(+) create mode 100644 dashboard/qt-app/main.py create mode 100644 dashboard/qt-app/mainwindow.py create mode 100644 dashboard/qt-app/mainwindow.ui create mode 100644 dashboard/qt-app/setup_leaderboard.py create mode 100644 dashboard/qt-app/waffle.py diff --git a/dashboard/qt-app/main.py b/dashboard/qt-app/main.py new file mode 100644 index 00000000..8c195549 --- /dev/null +++ b/dashboard/qt-app/main.py @@ -0,0 +1,11 @@ +import sys +from PyQt5.QtWidgets import QApplication + +from setup_leaderboard import Leaderboard + + +if __name__ == "__main__": + app = QApplication(sys.argv) + w = Leaderboard() + w.showFullScreen() + sys.exit(app.exec_()) diff --git a/dashboard/qt-app/mainwindow.py b/dashboard/qt-app/mainwindow.py new file mode 100644 index 00000000..6ae9d3c3 --- /dev/null +++ b/dashboard/qt-app/mainwindow.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '.\mainwindow.ui' +# +# Created by: PyQt5 UI code generator 5.15.4 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Leaderboard(object): + def setupUi(self, Leaderboard): + Leaderboard.setObjectName("Leaderboard") + Leaderboard.resize(800, 480) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Leaderboard.sizePolicy().hasHeightForWidth()) + Leaderboard.setSizePolicy(sizePolicy) + Leaderboard.setMinimumSize(QtCore.QSize(800, 480)) + Leaderboard.setMaximumSize(QtCore.QSize(2000, 1200)) + Leaderboard.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + Leaderboard.setStyleSheet("QWidget\n" +"{\n" +" color: rgb(0, 123, 255); \n" +" background-color: rgb(0, 0, 0);\n" +"\n" +"}\n" +"\n" +"QPushButton {\n" +" background-color: rgb(97, 97, 97);\n" +" color: rgb(255, 255, 255);\n" +" border-width: 1px;\n" +" border-color: rgb(97, 97, 97);\n" +" border-style: solid;\n" +" border-radius: 7;\n" +" padding: 3px;\n" +" padding-left: 5px;\n" +" padding-right: 5px;\n" +"}\n" +"\n" +"QPushButton:checked\n" +"{\n" +" color: rgb(255, 255, 255); \n" +" background-color: rgb(0, 123, 255);\n" +"}\n" +"\n" +"QProgressBar\n" +"{\n" +" background-color: rgb(166, 166, 166);\n" +" color: rgb(0, 0, 0);\n" +" border: 2px rgb(166, 166, 166);\n" +" border-radius: 5px;\n" +"}\n" +"\n" +"QProgressBar::chunk {\n" +" border: 2px rgb(166, 166, 166);\n" +" border-top-left-radius: 5px;\n" +" border-bottom-left-radius: 5px;\n" +" border-top-right-radius: 5px;\n" +" border-bottom-right-radius: 5px;\n" +" background-color: rgb(0, 123, 255);\n" +" /* width: 40px;\n" +" margin: 0.5px;*/\n" +"}\n" +"\n" +"#Labbruch {\n" +" color: rgb(239, 151, 0);\n" +"}") + self.centralwidget = QtWidgets.QWidget(Leaderboard) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.selectbtn = QtWidgets.QPushButton(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.selectbtn.sizePolicy().hasHeightForWidth()) + self.selectbtn.setSizePolicy(sizePolicy) + self.selectbtn.setMaximumSize(QtCore.QSize(70, 16777215)) + font = QtGui.QFont() + font.setPointSize(24) + font.setBold(True) + font.setWeight(75) + self.selectbtn.setFont(font) + self.selectbtn.setLayoutDirection(QtCore.Qt.LeftToRight) + self.selectbtn.setObjectName("selectbtn") + self.horizontalLayout.addWidget(self.selectbtn) + Leaderboard.setCentralWidget(self.centralwidget) + + self.retranslateUi(Leaderboard) + QtCore.QMetaObject.connectSlotsByName(Leaderboard) + + def retranslateUi(self, Leaderboard): + _translate = QtCore.QCoreApplication.translate + Leaderboard.setWindowTitle(_translate("Leaderboard", "Leaderboard")) + self.selectbtn.setText(_translate("Leaderboard", "C\n" +"h\n" +"a\n" +"n\n" +"g\n" +"e\n" +" \n" +"D\n" +"i\n" +"s\n" +"p\n" +"l\n" +"a\n" +"y")) diff --git a/dashboard/qt-app/mainwindow.ui b/dashboard/qt-app/mainwindow.ui new file mode 100644 index 00000000..a3b4c2f6 --- /dev/null +++ b/dashboard/qt-app/mainwindow.ui @@ -0,0 +1,135 @@ + + + Leaderboard + + + + 0 + 0 + 800 + 480 + + + + + 0 + 0 + + + + + 800 + 480 + + + + + 2000 + 1200 + + + + ArrowCursor + + + Leaderboard + + + QWidget +{ + color: rgb(0, 123, 255); + background-color: rgb(0, 0, 0); + +} + +QPushButton { + background-color: rgb(97, 97, 97); + color: rgb(255, 255, 255); + border-width: 1px; + border-color: rgb(97, 97, 97); + border-style: solid; + border-radius: 7; + padding: 3px; + padding-left: 5px; + padding-right: 5px; +} + +QPushButton:checked +{ + color: rgb(255, 255, 255); + background-color: rgb(0, 123, 255); +} + +QProgressBar +{ + background-color: rgb(166, 166, 166); + color: rgb(0, 0, 0); + border: 2px rgb(166, 166, 166); + border-radius: 5px; +} + +QProgressBar::chunk { + border: 2px rgb(166, 166, 166); + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + background-color: rgb(0, 123, 255); + /* width: 40px; + margin: 0.5px;*/ +} + +#Labbruch { + color: rgb(239, 151, 0); +} + + + + + + + + 0 + 0 + + + + + 70 + 16777215 + + + + + 24 + 75 + true + + + + Qt::LeftToRight + + + C +h +a +n +g +e + +D +i +s +p +l +a +y + + + + + + + + + diff --git a/dashboard/qt-app/setup_leaderboard.py b/dashboard/qt-app/setup_leaderboard.py new file mode 100644 index 00000000..e8bd164b --- /dev/null +++ b/dashboard/qt-app/setup_leaderboard.py @@ -0,0 +1,30 @@ + +import matplotlib +from mainwindow import Ui_Leaderboard + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMainWindow +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from waffle import generate_figure + +matplotlib.use('Qt5Agg') + + +class Leaderboard(QMainWindow, Ui_Leaderboard): + """ Opens the leaderboard """ + + def __init__(self): + super(Leaderboard, self).__init__() + self.setupUi(self) + self.setWindowFlags(Qt.FramelessWindowHint) + + self.selectbtn.clicked.connect(self.select) + + self.canvas = FigureCanvas(generate_figure("TEST")) + self.horizontalLayout.addWidget(self.canvas) + + def select(self): + print("clicked") + self.canvas.deleteLater() + self.canvas = FigureCanvas(generate_figure("Clicked")) + self.horizontalLayout.addWidget(self.canvas) diff --git a/dashboard/qt-app/waffle.py b/dashboard/qt-app/waffle.py new file mode 100644 index 00000000..13e393ae --- /dev/null +++ b/dashboard/qt-app/waffle.py @@ -0,0 +1,69 @@ +import math +import matplotlib +import matplotlib.pyplot as plt + +from pywaffle import Waffle + +matplotlib.rcParams.update({'text.color': "white", 'axes.labelcolor': "white"}) + + +def sort_dict_items(to_sort: dict): + dictionary_items = to_sort.items() + sorted_items = sorted(dictionary_items) + return {x[0]: x[1] for x in sorted_items} + + +def extract_data(sort: bool, data: dict): + if not data or sum(data.values()) < 3: + waffle_data = {"Cocktails trinken zum starten ...": 3} + else: + waffle_data = {f"{x} ({y})": y for x, y in data.items()} + if sort: + waffle_data = sort_dict_items(waffle_data) + return waffle_data + + +def generate_dimensions(total: float, count=True): + proportion = 3 + threshold_upper = 1.2 + threshold_lower = 0.77 + one_row_until = 9 + # use fixed grid for non count variables + if not count: + return {"rows": 10, "columns": 25} + # don't split until given dimension + if total < one_row_until: + return {"rows": 1} + row = max(math.floor(math.sqrt(total / proportion)), 1) + # calculate current proportion, add one if exceeds th + column = math.ceil(total / row) + real_prop = column / row + if real_prop >= proportion * threshold_upper: + row += 1 + # calculates adjusted proportion, rolls back if its too extreme + column = math.ceil(total / row) + adjusted_prop = column / row + if adjusted_prop <= proportion * threshold_lower: + row -= 1 + return {"rows": row} + + +def generate_figure(title: str, count=True, sort=True): + data = {"A": 10, "B": 5, "C": 15} + waffle_data = extract_data(sort, data) + dims = generate_dimensions(sum(data.values()), count) + fig = plt.figure( + FigureClass=Waffle, + **dims, + values=waffle_data, + title={ + 'label': title, + 'fontdict': { + 'fontsize': 50 + } + }, + facecolor=(0.054, 0.066, 0.090, 1), + legend={'loc': 'upper center', 'bbox_to_anchor': (0.5, 0.0), + 'ncol': 2, 'framealpha': 0, 'fontsize': 30} + ) + return fig From ffb610d3d5285e7bad086c9dccb037d7d5e84dfd Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Sat, 16 Oct 2021 16:06:14 +0200 Subject: [PATCH 33/65] Added model for endpoint --- dashboard/backend/main.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py index 5a9fb446..c64f6bf3 100644 --- a/dashboard/backend/main.py +++ b/dashboard/backend/main.py @@ -20,6 +20,12 @@ class Teaminfo(BaseModel): volume: int +class BoardConfig(BaseModel): + hourrange: Optional[int] = None + limit: Optional[int] = 5 + count: Optional[bool] = True + + @app.get("/") def home(): return {"message": "Welcome to dashboard api"} @@ -46,17 +52,14 @@ def get_leaderboard(hourrange=None, limit=2, count=True): sql = f"SELECT Team, {agg} as amount FROM Team{addition} GROUP BY Team ORDER BY {agg} DESC LIMIT ?" df = pd.read_sql(sql, conn, params=(limit,)) conn.close() - return_data = {x: y for x, y in zip(df.Team.to_list(), df.amount.to_list())} + return_data = dict(zip(df.Team.to_list(), df.amount.to_list())) return return_data @app.get("/leaderboard") -def leaderboard( - hourrange: Optional[int] = 24, - limit: Optional[int] = 5, - count: Optional[bool] = True -): - return get_leaderboard(hourrange, limit, count) +def leaderboard(conf: BoardConfig): + print(conf.hourrange, conf.limit, conf.count) + return get_leaderboard(conf.hourrange, conf.limit, conf.count) def create_tables(): From d0d03074d74755bc9d171cbb1c188fd074fa44db Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Sat, 16 Oct 2021 16:50:13 +0200 Subject: [PATCH 34/65] Added type cycle --- dashboard/qt-app/main.py | 1 + dashboard/qt-app/setup_leaderboard.py | 22 ++++++++++--- dashboard/qt-app/waffle.py | 47 +++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/dashboard/qt-app/main.py b/dashboard/qt-app/main.py index 8c195549..4ce2ae97 100644 --- a/dashboard/qt-app/main.py +++ b/dashboard/qt-app/main.py @@ -8,4 +8,5 @@ app = QApplication(sys.argv) w = Leaderboard() w.showFullScreen() + # w.update(15) sys.exit(app.exec_()) diff --git a/dashboard/qt-app/setup_leaderboard.py b/dashboard/qt-app/setup_leaderboard.py index e8bd164b..956c1f0f 100644 --- a/dashboard/qt-app/setup_leaderboard.py +++ b/dashboard/qt-app/setup_leaderboard.py @@ -1,9 +1,11 @@ - +from itertools import cycle +import time import matplotlib from mainwindow import Ui_Leaderboard from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMainWindow +from PyQt5.QtWidgets import QMainWindow, qApp + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from waffle import generate_figure @@ -19,12 +21,22 @@ def __init__(self): self.setWindowFlags(Qt.FramelessWindowHint) self.selectbtn.clicked.connect(self.select) + self.options = cycle([1, 2, 3, 4]) + self.curr_option = next(self.options) - self.canvas = FigureCanvas(generate_figure("TEST")) + fig = generate_figure(self.curr_option) + self.canvas = FigureCanvas(fig) self.horizontalLayout.addWidget(self.canvas) def select(self): - print("clicked") self.canvas.deleteLater() - self.canvas = FigureCanvas(generate_figure("Clicked")) + self.curr_option = next(self.options) + self.canvas = FigureCanvas(generate_figure(self.curr_option)) + self.horizontalLayout.addWidget(self.canvas) + + def update(self, timing=15): + self.canvas.deleteLater() + self.canvas = FigureCanvas(generate_figure(self.curr_option)) self.horizontalLayout.addWidget(self.canvas) + time.sleep(timing) + qApp.processEvents() diff --git a/dashboard/qt-app/waffle.py b/dashboard/qt-app/waffle.py index 13e393ae..92267c4e 100644 --- a/dashboard/qt-app/waffle.py +++ b/dashboard/qt-app/waffle.py @@ -1,4 +1,6 @@ import math +import json +import requests import matplotlib import matplotlib.pyplot as plt @@ -7,6 +9,14 @@ matplotlib.rcParams.update({'text.color': "white", 'axes.labelcolor': "white"}) +NAMES = [ + "Anzahl (Heute)", + "Volumen (Heute)", + "Anzahl (Komplett)", + "Volumen (Komplett)" +] + + def sort_dict_items(to_sort: dict): dictionary_items = to_sort.items() sorted_items = sorted(dictionary_items) @@ -48,8 +58,39 @@ def generate_dimensions(total: float, count=True): return {"rows": row} -def generate_figure(title: str, count=True, sort=True): - data = {"A": 10, "B": 5, "C": 15} +def get_data(count: bool, hourrange: int, limit: int): + headers = {"content-type": "application/json"} + payload = {"limit": limit, "count": count, "hourrange": hourrange} + payload = json.dumps(payload) + res = requests.get("http://127.0.0.1:8080/leaderboard", data=payload, headers=headers) + return json.loads(res.text) + + +def decide_data(datatype: int): + count = True + sort = True + hourrange = None + if datatype in (1, 2): + hourrange = 24 + limit = 5 + if datatype in (3, 4): + sort = False + limit = 10 + if datatype in (2, 4): + count = False + return (count, sort, hourrange, limit) + + +def generate_figure(datatype: int): + """Generates the Waffle plot. + Type is int from 1-4: + 1: Amount Today + 2: Volume Today + 3: Amount All Time + 4: Volume All Time + """ + count, sort, hourrange, limit = decide_data(datatype) + data = get_data(count, hourrange, limit) waffle_data = extract_data(sort, data) dims = generate_dimensions(sum(data.values()), count) fig = plt.figure( @@ -57,7 +98,7 @@ def generate_figure(title: str, count=True, sort=True): **dims, values=waffle_data, title={ - 'label': title, + 'label': NAMES[datatype - 1], 'fontdict': { 'fontsize': 50 } From 477fc998a2c9c5b5d247343f73133985d1836591 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 13:06:39 +0200 Subject: [PATCH 35/65] Added more docs --- config/config_manager.py | 2 + docs/diagrams/FirstSchema.puml | 17 +- docs/diagrams/out/ProgramSchema.svg | 509 ++++++++++++++++++++++++---- 3 files changed, 450 insertions(+), 78 deletions(-) diff --git a/config/config_manager.py b/config/config_manager.py index 458c64d4..8c4eec98 100644 --- a/config/config_manager.py +++ b/config/config_manager.py @@ -23,6 +23,7 @@ class ConfigManager: MICROSERVICE_BASE_URL = "http://127.0.0.1:5000" # if to use the teams function and according options. # URL should be 'device_ip:8080' where dashboard container is running and in the same network + # Button names must be two strings in the list USE_TEAMS = True TEAM_BUTTON_NAMES = ["Team 1", "Team 2"] TEAM_API_URL = "http://127.0.0.1:8080" @@ -38,6 +39,7 @@ def __init__(self): self.make_cocktail = True self.supress_error = False self.old_ingredient = [] + self.selected_team = "Nothing" shared = Shared() diff --git a/docs/diagrams/FirstSchema.puml b/docs/diagrams/FirstSchema.puml index ecbbb2ff..d001b235 100644 --- a/docs/diagrams/FirstSchema.puml +++ b/docs/diagrams/FirstSchema.puml @@ -10,13 +10,14 @@ !include DEVICONS/sqllite.puml !include DEVICONS/aptana.puml !include DEVICONS/docker.puml +!include DEVICONS/html5.puml !include FONTAWESOME/users.puml !include FONTAWESOME/user.puml !include FONTAWESOMEOLD/send_o.puml skinparam backgroundColor #fff -LAYOUT_WITH_LEGEND() +HIDE_STEREOTYPE() Container(configmanager, "Config Manager", "python", "Class to hold all configuration, used where needed for configs", "python") Container(displaycontroller, "Display Controller", "python", "Class to get values from GUI", "python") @@ -37,7 +38,11 @@ LAYOUT_WITH_LEGEND() Person_Ext(user, "User", "Person who wants the exported data from the DB", "user") System(email, "GMail", "Gmail account / service used as sender over smtp for the data", "send_o") -Rel_U(dbhandler, db, "Reads/Writes") + Container(dashapi, "Dashboard API", "python", "FastAPI to get team Data for the leaderboard", "docker") + Container(dashfrontend, "Dashboard", "Streamlit", "Python Streamlit Page to show leaderboard", "html5") + + +Rel(dbhandler, db, "Reads/Writes") Rel(mainwindow, dialogwindow, "opens") Rel(mainwindow, progresscreen, "opens") @@ -48,12 +53,16 @@ Rel(mainwindow, savehandler, "calls") ' Rel(rpicontroller, configmanager, "uses") ' Rel(displaycontroller, configmanager, "uses") -Rel_U(apicontroller, displaycontroller, "uses") +Rel(apicontroller, displaycontroller, "uses") Rel(apicontroller, displayhandler, "uses") -Rel_L(apicontroller, dbhandler, "uses") +Rel(apicontroller, dbhandler, "uses") Rel(apicontroller, rpicontroller, "calls") Rel(apicontroller, microservice, "post", "http") +Rel_L(apicontroller, dashapi, "post", "http") +Rel(dashfrontend, dashapi, "uses", "http") + + Rel(microservice, webhook, "post", "https") Rel(microservice, email, "uses", "smtp") diff --git a/docs/diagrams/out/ProgramSchema.svg b/docs/diagrams/out/ProgramSchema.svg index fbb672b3..b3422f70 100644 --- a/docs/diagrams/out/ProgramSchema.svg +++ b/docs/diagrams/out/ProgramSchema.svg @@ -1,32 +1,36 @@ -Config Manager[python]Class to hold all configuration,used where needed for configsDisplay Controller[python]Class to get values from GUIDisplay Handler[python]Class to set values / propertiesin GUIRPI Controller[python]Class to controll the RaspberryPi and the GPIOsAPI Controller[python]Modules to handle logic forseperate viewsSave Handler[python]Class to export data from DBto csvMicroservice[python]Flask API to call services tointeract with the wwwWebhooksExternal webhooks orendpoints to post data from thecocktailmaker toDatabase Handler[Python]Handles and executes all SQLlogic and connectionDatabase[Sqllite]Holds data for cocktails, bottlesand ingredientsMainwindow[PyQt]The main user interactionhappens hereDialog windows[PyQt]User interaction popups forinput / outputProgress Screen[PyQt]View for progress andinterruption of cocktailUserPerson who wants theexported data from the DBGMailGmail account / service usedas sender over smtp for thedataReads/Writesopensopenscallscallsusesusesusescallspost[http]post[https]uses[smtp]sends[smtp]Legendpersonsystemcontainerexternal personexternal systemexternal containerConfig Manager[python]Class to hold all configuration,used where needed for configsDisplay Controller[python]Class to get values from GUIDisplay Handler[python]Class to set values / propertiesin GUIRPI Controller[python]Class to controll the RaspberryPi and the GPIOsAPI Controller[python]Modules to handle logic forseperate viewsSave Handler[python]Class to export data from DBto csvMicroservice[python]Flask API to call services tointeract with the wwwWebhooksExternal webhooks orendpoints to post data from thecocktailmaker toDatabase Handler[Python]Handles and executes all SQLlogic and connectionDatabase[Sqllite]Holds data for cocktails, bottlesand ingredientsMainwindow[PyQt]The main user interactionhappens hereDialog windows[PyQt]User interaction popups forinput / outputProgress Screen[PyQt]View for progress andinterruption of cocktailUserPerson who wants theexported data from the DBGMailGmail account / service usedas sender over smtp for thedataDashboard API[python]FastAPI to get team Data forthe leaderboardDashboard[Streamlit]Python Streamlit Page to showleaderboardReads/Writesopensopenscallscallsusesusesusescallspost[http]post[http]uses[http]post[https]uses[smtp]sends[smtp]> false +skinparam rectangle<> { + backgroundcolor #00000000 + bordercolor #00000000 +} + skinparam rectangle { StereotypeFontSize 12 shadowing false @@ -121,17 +149,37 @@ skinparam queue { shadowing false } -skinparam Arrow { +skinparam arrow { Color #666666 FontColor #666666 FontSize 12 } +skinparam actor { + StereotypeFontSize 12 + shadowing false + style awesome +} + +skinparam person { + StereotypeFontSize 12 + shadowing false +} + +skinparam package { + StereotypeFontSize 6 + StereotypeFontColor transparent + FontStyle plain + BackgroundColor transparent +} + skinparam rectangle<> { Shadowing false - StereotypeFontSize 0 + StereotypeFontSize 6 + StereotypeFontColor transparent FontColor #444444 BorderColor #444444 + BackgroundColor transparent BorderStyle dashed } @@ -154,6 +202,78 @@ skinparam rectangle<> { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -169,6 +289,31 @@ skinparam rectangle<> { BackgroundColor #08427B BorderColor #073B6F } +skinparam database<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #08427B + BorderColor #073B6F +} +skinparam queue<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #08427B + BorderColor #073B6F +} +skinparam actor<> { + StereotypeFontColor #08427B + FontColor #08427B + BackgroundColor #08427B + BorderColor #073B6F +} +skinparam person<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #08427B + BorderColor #073B6F +} + skinparam rectangle<> { StereotypeFontColor #FFFFFF @@ -176,6 +321,31 @@ skinparam rectangle<> { BackgroundColor #686868 BorderColor #8A8A8A } +skinparam database<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #686868 + BorderColor #8A8A8A +} +skinparam queue<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #686868 + BorderColor #8A8A8A +} +skinparam actor<> { + StereotypeFontColor #686868 + FontColor #686868 + BackgroundColor #686868 + BorderColor #8A8A8A +} +skinparam person<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #686868 + BorderColor #8A8A8A +} + skinparam rectangle<> { StereotypeFontColor #FFFFFF @@ -183,6 +353,31 @@ skinparam rectangle<> { BackgroundColor #1168BD BorderColor #3C7FC0 } +skinparam database<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #1168BD + BorderColor #3C7FC0 +} +skinparam queue<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #1168BD + BorderColor #3C7FC0 +} +skinparam actor<> { + StereotypeFontColor #1168BD + FontColor #1168BD + BackgroundColor #1168BD + BorderColor #3C7FC0 +} +skinparam person<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #1168BD + BorderColor #3C7FC0 +} + skinparam rectangle<> { StereotypeFontColor #FFFFFF @@ -190,6 +385,34 @@ skinparam rectangle<> { BackgroundColor #999999 BorderColor #8A8A8A } +skinparam database<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #999999 + BorderColor #8A8A8A +} +skinparam queue<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #999999 + BorderColor #8A8A8A +} +skinparam actor<> { + StereotypeFontColor #999999 + FontColor #999999 + BackgroundColor #999999 + BorderColor #8A8A8A +} +skinparam person<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #999999 + BorderColor #8A8A8A +} + + + + sprite $person [48x48/16] { 000000000000000000000000000000000000000000000000 @@ -242,6 +465,67 @@ sprite $person [48x48/16] { 000000000000000000000000000000000000000000000000 } +sprite $person2 [48x48/16] { +0000000000000000000049BCCA7200000000000000000000 +0000000000000000006EFFFFFFFFB3000000000000000000 +00000000000000001CFFFFFFFFFFFF700000000000000000 +0000000000000001EFFFFFFFFFFFFFF80000000000000000 +000000000000000CFFFFFFFFFFFFFFFF6000000000000000 +000000000000007FFFFFFFFFFFFFFFFFF100000000000000 +00000000000001FFFFFFFFFFFFFFFFFFF900000000000000 +00000000000006FFFFFFFFFFFFFFFFFFFF00000000000000 +0000000000000BFFFFFFFFFFFFFFFFFFFF40000000000000 +0000000000000EFFFFFFFFFFFFFFFFFFFF70000000000000 +0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000 +0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000 +0000000000000DFFFFFFFFFFFFFFFFFFFF60000000000000 +0000000000000AFFFFFFFFFFFFFFFFFFFF40000000000000 +00000000000006FFFFFFFFFFFFFFFFFFFE00000000000000 +00000000000000EFFFFFFFFFFFFFFFFFF800000000000000 +000000000000007FFFFFFFFFFFFFFFFFF100000000000000 +000000000000000BFFFFFFFFFFFFFFFF5000000000000000 +0000000000000001DFFFFFFFFFFFFFF70000000000000000 +00000000000000000BFFFFFFFFFFFF500000000000000000 +0000000000000000005DFFFFFFFFA1000000000000000000 +0000000000000000000037ABB96100000000000000000000 +000000000002578888300000000005888864100000000000 +0000000007DFFFFFFFFD9643347BFFFFFFFFFB4000000000 +00000004EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB10000000 +0000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2000000 +000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE100000 +00003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000 +0000BFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50000 +0003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0000 +0009FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2000 +000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6000 +000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000 +001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000 +001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000 +001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000 +001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA000 +000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000 +000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6000 +0009FFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFFF2000 +0003FFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFFD0000 +0000BFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFF50000 +00003FFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFB00000 +000006FFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFE100000 +0000007FFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFD2000000 +00000004EFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFB10000000 +0000000007DF8FFFFFFFFFFFFFFFFFFFFFF8FB4000000000 +000000000002578888888888888888888864100000000000 +} + + + + + + + + + + + @@ -263,20 +547,31 @@ skinparam rectangle<> { BackgroundColor #438DD5 BorderColor #3C7FC0 } - skinparam database<> { StereotypeFontColor #FFFFFF FontColor #FFFFFF BackgroundColor #438DD5 BorderColor #3C7FC0 } - skinparam queue<> { StereotypeFontColor #FFFFFF FontColor #FFFFFF BackgroundColor #438DD5 BorderColor #3C7FC0 } +skinparam actor<> { + StereotypeFontColor #438DD5 + FontColor #438DD5 + BackgroundColor #438DD5 + BorderColor #3C7FC0 +} +skinparam person<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #438DD5 + BorderColor #3C7FC0 +} + skinparam rectangle<> { StereotypeFontColor #FFFFFF @@ -284,20 +579,34 @@ skinparam rectangle<> { BackgroundColor #B3B3B3 BorderColor #A6A6A6 } - skinparam database<> { StereotypeFontColor #FFFFFF FontColor #FFFFFF BackgroundColor #B3B3B3 BorderColor #A6A6A6 } - skinparam queue<> { StereotypeFontColor #FFFFFF FontColor #FFFFFF BackgroundColor #B3B3B3 BorderColor #A6A6A6 } +skinparam actor<> { + StereotypeFontColor #B3B3B3 + FontColor #B3B3B3 + BackgroundColor #B3B3B3 + BorderColor #A6A6A6 +} +skinparam person<> { + StereotypeFontColor #FFFFFF + FontColor #FFFFFF + BackgroundColor #B3B3B3 + BorderColor #A6A6A6 +} + + + + @@ -577,6 +886,59 @@ sprite $docker [48x48/16] { skinparam folderBackgroundColor<> White +sprite $html5 [48x48/16] { +000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000 +0000000DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD0000000 +0000000EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0000000 +0000000CFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0000000 +0000000BFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0000000 +00000009FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90000000 +00000008FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000 +00000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60000000 +00000005FFFFF7333333333333333333338FFFFF50000000 +00000004FFFFF7000000000000000000007FFFFF40000000 +00000002FFFFF8000000000000000000008FFFFF20000000 +00000001FFFFF900000000000000000000AFFFFF10000000 +00000000FFFFFB00008888888888888888DFFFFF00000000 +00000000EFFFFC0000EFFFFFFFFFFFFFFFFFFFFE00000000 +00000000CFFFFE0000DFFFFFFFFFFFFFFFFFFFFC00000000 +00000000BFFFFF0000CFFFFFFFFFFFFFFFFFFFFB00000000 +00000000AFFFFF10009DDDDDDDDDDDDDDEFFFFFA00000000 +000000008FFFFF20000000000000000002FFFFF800000000 +000000007FFFFF30000000000000000004FFFFF700000000 +000000005FFFFF50000000000000000005FFFFF500000000 +000000004FFFFF60000000000000000007FFFFF400000000 +000000003FFFFFEDDDDDDDDDDDDDD20008FFFFF300000000 +000000001FFFFFFFFFFFFFFFFFFFF1000AFFFFF100000000 +000000000FFFFFC4444FFFFFFFFFF0000BFFFFF000000000 +000000000EFFFFC0000FFFFFFFFFE0000CFFFFE000000000 +000000000DFFFFD0000DFFFFFFFFD0000EFFFFD000000000 +000000000BFFFFF00009FFFFFFFF90000FFFFFB000000000 +000000000AFFFFF00000048CC84000001FFFFFA000000000 +0000000009FFFFF200000000000000002FFFFF8000000000 +0000000007FFFFF300000000000000004FFFFF7000000000 +0000000006FFFFFFB73000000000026AFFFFFF6000000000 +0000000004FFFFFFFFFEA5100049EFFFFFFFFF4000000000 +0000000003FFFFFFFFFFFFFCCFFFFFFFFFFFFF3000000000 +0000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFF1000000000 +0000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000 +0000000000EFFFFFFFFFFFFFFFFFFFFFFFFFFE0000000000 +0000000000038CFFFFFFFFFFFFFFFFFFFFD9400000000000 +0000000000000015AEFFFFFFFFFFFFEA6100000000000000 +00000000000000000037BFFFFFFC84000000000000000000 +000000000000000000000159950000000000000000000000 +000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000 +} + + +skinparam folderBackgroundColor<> White sprite $users [48x48/16] { 000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000 @@ -740,36 +1102,31 @@ skinparam folderBackgroundColor<> White skinparam backgroundColor #fff hide stereotype -legend right -|**Legend** | -|<#08427B> person | -|<#1168BD> system | -|<#438DD5> container | -|<#686868> external person | -|<#999999> external system | -|<#B3B3B3> external container | -endlegend - - rectangle "<$python>\n==Config Manager\n//[python]//\n\n Class to hold all configuration, used where needed for configs" <> as configmanager - rectangle "<$python>\n==Display Controller\n//[python]//\n\n Class to get values from GUI" <> as displaycontroller - rectangle "<$python>\n==Display Handler\n//[python]//\n\n Class to set values / properties in GUI" <> as displayhandler - rectangle "<$python>\n==RPI Controller\n//[python]//\n\n Class to controll the Raspberry Pi and the GPIOs" <> as rpicontroller - rectangle "<$python>\n==API Controller\n//[python]//\n\n Modules to handle logic for seperate views" <> as apicontroller - rectangle "<$python>\n==Save Handler\n//[python]//\n\n Class to export data from DB to csv" <> as savehandler - - rectangle "<$docker>\n==Microservice\n//[python]//\n\n Flask API to call services to interact with the www" <> as microservice - rectangle "<$send_o>\n==Webhooks\n\n External webhooks or endpoints to post data from the cocktailmaker to" <> as webhook - - rectangle "<$python>\n==Database Handler\n//[Python]//\n\n Handles and executes all SQL logic and connection" <> as dbhandler - database "<$sqllite>\n==Database\n//[Sqllite]//\n\n Holds data for cocktails, bottles and ingredients" <> as db - - rectangle "<$python>\n==Mainwindow\n//[PyQt]//\n\n The main user interaction happens here" <> as mainwindow - rectangle "<$python>\n==Dialog windows\n//[PyQt]//\n\n User interaction popups for input / output" <> as dialogwindow - rectangle "<$python>\n==Progress Screen\n//[PyQt]//\n\n View for progress and interruption of cocktail" <> as progresscreen - rectangle "<$user>\n==User\n\n Person who wants the exported data from the DB" <> as user - rectangle "<$send_o>\n==GMail\n\n Gmail account / service used as sender over smtp for the data" <> as email - -dbhandler -UP->> db : **Reads/Writes** + + rectangle "<$python>\n==Config Manager\n//[python]//\n\n Class to hold all configuration, used where needed for configs" <> as configmanager + rectangle "<$python>\n==Display Controller\n//[python]//\n\n Class to get values from GUI" <> as displaycontroller + rectangle "<$python>\n==Display Handler\n//[python]//\n\n Class to set values / properties in GUI" <> as displayhandler + rectangle "<$python>\n==RPI Controller\n//[python]//\n\n Class to controll the Raspberry Pi and the GPIOs" <> as rpicontroller + rectangle "<$python>\n==API Controller\n//[python]//\n\n Modules to handle logic for seperate views" <> as apicontroller + rectangle "<$python>\n==Save Handler\n//[python]//\n\n Class to export data from DB to csv" <> as savehandler + + rectangle "<$docker>\n==Microservice\n//[python]//\n\n Flask API to call services to interact with the www" <> as microservice + rectangle "<$send_o>\n==Webhooks\n\n External webhooks or endpoints to post data from the cocktailmaker to" <> as webhook + + rectangle "<$python>\n==Database Handler\n//[Python]//\n\n Handles and executes all SQL logic and connection" <> as dbhandler + database "<$sqllite>\n==Database\n//[Sqllite]//\n\n Holds data for cocktails, bottles and ingredients" <> as db + + rectangle "<$python>\n==Mainwindow\n//[PyQt]//\n\n The main user interaction happens here" <> as mainwindow + rectangle "<$python>\n==Dialog windows\n//[PyQt]//\n\n User interaction popups for input / output" <> as dialogwindow + rectangle "<$python>\n==Progress Screen\n//[PyQt]//\n\n View for progress and interruption of cocktail" <> as progresscreen + rectangle "<$user>\n==User\n\n Person who wants the exported data from the DB" <> as user + rectangle "<$send_o>\n==GMail\n\n Gmail account / service used as sender over smtp for the data" <> as email + + rectangle "<$docker>\n==Dashboard API\n//[python]//\n\n FastAPI to get team Data for the leaderboard" <> as dashapi + rectangle "<$html5>\n==Dashboard\n//[Streamlit]//\n\n Python Streamlit Page to show leaderboard" <> as dashfrontend + + +dbhandler - ->> db : **Reads/Writes** mainwindow - ->> dialogwindow : **opens** mainwindow - ->> progresscreen : **opens** @@ -777,12 +1134,16 @@ mainwindow - ->> apicontroller : **calls** mainwindow - ->> savehandler : **calls** -apicontroller -UP->> displaycontroller : **uses** +apicontroller - ->> displaycontroller : **uses** apicontroller - ->> displayhandler : **uses** -apicontroller -LEFT->> dbhandler : **uses** +apicontroller - ->> dbhandler : **uses** apicontroller - ->> rpicontroller : **calls** apicontroller - ->> microservice : **post**\n//[http]// +apicontroller -LEFT->> dashapi : **post**\n//[http]// +dashfrontend - ->> dashapi : **uses**\n//[http]// + + microservice - ->> webhook : **post**\n//[https]// microservice - ->> email : **uses**\n//[smtp]// @@ -791,10 +1152,10 @@ email - ->> user : **sends**\n//[smtp]// @enduml -PlantUML version 1.2020.22(Sun Dec 06 10:36:27 CET 2020) +PlantUML version 1.2021.7(Sun May 23 14:40:07 CEST 2021) (GPL source distribution) -Java Runtime: OpenJDK Runtime Environment -JVM: OpenJDK 64-Bit Server VM +Java Runtime: Java(TM) SE Runtime Environment +JVM: Java HotSpot(TM) 64-Bit Server VM Default Encoding: Cp1252 Language: de Country: DE From c75682ee025e247ab6bbf4c9f0ac62ffcfcae59a Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 13:09:23 +0200 Subject: [PATCH 36/65] Added team window to main programm --- src_ui/setup_team_window.py | 27 ++++++ ui_elements/teamselection.py | 103 ++++++++++++++++++++++ ui_elements/teamselection.ui | 161 +++++++++++++++++++++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 src_ui/setup_team_window.py create mode 100644 ui_elements/teamselection.py create mode 100644 ui_elements/teamselection.ui diff --git a/src_ui/setup_team_window.py b/src_ui/setup_team_window.py new file mode 100644 index 00000000..faa7ecfc --- /dev/null +++ b/src_ui/setup_team_window.py @@ -0,0 +1,27 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QDialog + +from ui_elements.teamselection import Ui_Teamselection +from config.config_manager import ConfigManager, shared + + +class TeamScreen(QDialog, Ui_Teamselection, ConfigManager): + """ Class for the Team selection Screen. """ + + def __init__(self, parent=None): + super(TeamScreen, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.FramelessWindowHint) + self.PBteamone.clicked.connect(lambda: set_team(self.TEAM_BUTTON_NAMES[0])) + self.PBteamone.setText(self.TEAM_BUTTON_NAMES[0]) + self.PBteamtwo.clicked.connect(lambda: set_team(self.TEAM_BUTTON_NAMES[1])) + self.PBteamtwo.setText(self.TEAM_BUTTON_NAMES[1]) + self.setWindowIcon(QIcon(parent.icon_path)) + self.mainscreen = parent + if not self.mainscreen.DEVENVIRONMENT: + self.setCursor(Qt.BlankCursor) + + def set_team(team: str): + shared.selected_team = team + self.close() diff --git a/ui_elements/teamselection.py b/ui_elements/teamselection.py new file mode 100644 index 00000000..43bc1bdc --- /dev/null +++ b/ui_elements/teamselection.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '.\teamselection.ui' +# +# Created by: PyQt5 UI code generator 5.15.4 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Teamselection(object): + def setupUi(self, Teamselection): + Teamselection.setObjectName("Teamselection") + Teamselection.resize(800, 480) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Teamselection.sizePolicy().hasHeightForWidth()) + Teamselection.setSizePolicy(sizePolicy) + Teamselection.setMinimumSize(QtCore.QSize(800, 480)) + Teamselection.setMaximumSize(QtCore.QSize(800, 480)) + Teamselection.setStyleSheet("QWidget {\n" +" color: rgb(0, 123, 255); \n" +" background-color: rgb(0, 0, 0);\n" +"}\n" +"\n" +"QPushButton {\n" +" color: rgb(255, 255, 255);\n" +" border-width: 1px;\n" +" border-style: solid;\n" +" border-radius: 40;\n" +" padding: 3px;\n" +" padding-left: 5px;\n" +" padding-right: 5px;\n" +"}\n" +"\n" +"#PBteamone {\n" +" background-color: rgb(235, 64, 52);\n" +" border-color: rgb(235, 64, 52);\n" +"}\n" +"\n" +"#PBteamtwo {\n" +" background-color: rgb(52, 76, 235);\n" +" border-color: rgb(52, 76, 235);\n" +"}\n" +"\n" +"QPushButton:checked {\n" +" color: rgb(255, 255, 255); \n" +" background-color: rgb(52, 235, 143);\n" +"}\n" +"\n" +"\n" +"") + Teamselection.setModal(True) + self.verticalLayout = QtWidgets.QVBoxLayout(Teamselection) + self.verticalLayout.setObjectName("verticalLayout") + self.Lheader = QtWidgets.QLabel(Teamselection) + self.Lheader.setMinimumSize(QtCore.QSize(0, 100)) + self.Lheader.setMaximumSize(QtCore.QSize(16777215, 100)) + font = QtGui.QFont() + font.setPointSize(32) + font.setBold(True) + font.setWeight(75) + self.Lheader.setFont(font) + self.Lheader.setAlignment(QtCore.Qt.AlignCenter) + self.Lheader.setObjectName("Lheader") + self.verticalLayout.addWidget(self.Lheader) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.PBteamone = QtWidgets.QPushButton(Teamselection) + self.PBteamone.setMinimumSize(QtCore.QSize(300, 350)) + self.PBteamone.setMaximumSize(QtCore.QSize(450, 16777215)) + font = QtGui.QFont() + font.setPointSize(36) + font.setBold(True) + font.setWeight(75) + self.PBteamone.setFont(font) + self.PBteamone.setObjectName("PBteamone") + self.horizontalLayout.addWidget(self.PBteamone) + self.PBteamtwo = QtWidgets.QPushButton(Teamselection) + self.PBteamtwo.setMinimumSize(QtCore.QSize(300, 350)) + self.PBteamtwo.setMaximumSize(QtCore.QSize(450, 16777215)) + font = QtGui.QFont() + font.setPointSize(36) + font.setBold(True) + font.setWeight(75) + self.PBteamtwo.setFont(font) + self.PBteamtwo.setObjectName("PBteamtwo") + self.horizontalLayout.addWidget(self.PBteamtwo) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Teamselection) + QtCore.QMetaObject.connectSlotsByName(Teamselection) + + def retranslateUi(self, Teamselection): + _translate = QtCore.QCoreApplication.translate + Teamselection.setWindowTitle(_translate("Teamselection", "~~ Teamwahl ~~")) + self.Lheader.setText(_translate("Teamselection", "Team auswählen")) + self.PBteamone.setText(_translate("Teamselection", "Team #1")) + self.PBteamtwo.setText(_translate("Teamselection", "Team #2")) diff --git a/ui_elements/teamselection.ui b/ui_elements/teamselection.ui new file mode 100644 index 00000000..1783fb3c --- /dev/null +++ b/ui_elements/teamselection.ui @@ -0,0 +1,161 @@ + + + Teamselection + + + + 0 + 0 + 800 + 480 + + + + + 0 + 0 + + + + + 800 + 480 + + + + + 800 + 480 + + + + ~~ Teamwahl ~~ + + + QWidget { + color: rgb(0, 123, 255); + background-color: rgb(0, 0, 0); +} + +QPushButton { + color: rgb(255, 255, 255); + border-width: 1px; + border-style: solid; + border-radius: 40; + padding: 3px; + padding-left: 5px; + padding-right: 5px; +} + +#PBteamone { + background-color: rgb(235, 64, 52); + border-color: rgb(235, 64, 52); +} + +#PBteamtwo { + background-color: rgb(52, 76, 235); + border-color: rgb(52, 76, 235); +} + +QPushButton:checked { + color: rgb(255, 255, 255); + background-color: rgb(52, 235, 143); +} + + + + + + true + + + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + + 32 + 75 + true + + + + Team auswählen + + + Qt::AlignCenter + + + + + + + + + + 300 + 350 + + + + + 450 + 16777215 + + + + + 36 + 75 + true + + + + Team #1 + + + + + + + + 300 + 350 + + + + + 450 + 16777215 + + + + + 36 + 75 + true + + + + Team #2 + + + + + + + + + + From 7b8b6ef1cfbd06d1929aa9244d41dee8fff24f33 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 13:10:04 +0200 Subject: [PATCH 37/65] Implemented team window to logic --- src_ui/setup_mainwindow.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src_ui/setup_mainwindow.py b/src_ui/setup_mainwindow.py index 3edfd674..f65f5d94 100644 --- a/src_ui/setup_mainwindow.py +++ b/src_ui/setup_mainwindow.py @@ -17,6 +17,7 @@ from src.display_handler import DisplayHandler from src.database_commander import DatabaseCommander from src.logger_handler import LoggerHandler +from src_ui.setup_team_window import TeamScreen from ui_elements.Cocktailmanager_2 import Ui_MainWindow from src_ui.setup_progress_screen import ProgressScreen @@ -61,6 +62,7 @@ def __init__(self, parent=None): self.ingd: GetIngredientWindow = None self.handw: HandaddWidget = None self.availw: AvailableWindow = None + self.teamw: TeamScreen = None def passwordwindow(self, le_to_write, x_pos=0, y_pos=0, headertext=None): """ Opens up the PasswordScreen connected to the lineedit offset from the left upper side """ @@ -83,6 +85,10 @@ def progressionqwindow(self, labelchange=""): self.prow.Lheader.setText(labelchange) self.prow.show() + def teamwindow(self): + self.teamw = TeamScreen(self) + self.teamw.exec_() + def prow_change(self, pbvalue): """ Changes the value of the Progressionbar of the ProBarWindow. """ self.prow.progressBar.setValue(pbvalue) From 47efa3c2a1b52bdfb15b02a30284442e1630333b Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 13:18:24 +0200 Subject: [PATCH 38/65] Implemented teams to rpi controller and added docs --- src/rpi_controller.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/rpi_controller.py b/src/rpi_controller.py index c5538452..70f5d59b 100644 --- a/src/rpi_controller.py +++ b/src/rpi_controller.py @@ -18,6 +18,9 @@ def __init__(self): self.devenvironment = True def clean_pumps(self): + """Clean the pumps for the defined time in the config. + Acitvates all pumps for the given time + """ active_pins = self.USEDPINS[: self.NUMBER_BOTTLES] self.activate_pinlist(active_pins) t_cleaned = 0 @@ -30,6 +33,22 @@ def clean_pumps(self): self.close_pinlist(active_pins) def make_cocktail(self, w, bottle_list: list[int], volume_list: list[float], labelchange=""): + """RPI Logic to prepare the cocktail. + Calculates needed time for each slot according to data and config. + Updates Progressbar status. Returns data for DB updates. + + Args: + w (QtMainWindow): MainWindow Object + bottle_list (list[int]): Number of bottles to be used + volume_list (list[float]): Corresponding Volumens needed of bottles + labelchange (str, optional): Option to change the display text of Progress Screen. Defaults to "". + + Returns: + tuple(list[int], float, float): Consumption of each bottle, taken time, max needed time + """ + # Only shwo team dialog if it is enabled + if self.USE_TEAMS: + w.teamwindow() shared.cocktail_started = True shared.make_cocktail = True w.progressionqwindow(labelchange) From bbad2c91a73832bf418186dde97ae5819e3801a1 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 16:49:06 +0200 Subject: [PATCH 39/65] Adjusted poetry for dashboard --- dashboard/poetry.lock | 66 +++++++++++++++++++++++++++++++++++++++- dashboard/pyproject.toml | 3 ++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/dashboard/poetry.lock b/dashboard/poetry.lock index 3da89df7..3847ecce 100644 --- a/dashboard/poetry.lock +++ b/dashboard/poetry.lock @@ -833,6 +833,34 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "pyqt5" +version = "5.15.4" +description = "Python bindings for the Qt cross platform application toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +PyQt5-Qt5 = ">=5.15" +PyQt5-sip = ">=12.8,<13" + +[[package]] +name = "pyqt5-qt5" +version = "5.15.2" +description = "The subset of a Qt installation needed by PyQt5." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyqt5-sip" +version = "12.9.0" +description = "The sip module support for PyQt5" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "pyrsistent" version = "0.18.0" @@ -1189,7 +1217,7 @@ notebook = ">=4.4.1" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "d7013cef5fa9b5a36631b4b407b42d681be32f8c7ac8a86498c7e447e1095abb" +content-hash = "e4511194475ffebac850f4a86cc057a82b4af2f5860634687a8584ea0239840c" [metadata.files] altair = [ @@ -1806,6 +1834,42 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +pyqt5 = [ + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:8c0848ba790a895801d5bfd171da31cad3e551dbcc4e59677a3b622de2ceca98"}, + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:883a549382fc22d29a0568f3ef20b38c8e7ab633a59498ac4eb63a3bf36d3fd3"}, + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:a88526a271e846e44779bb9ad7a738c6d3c4a9d01e15a128ecfc6dd4696393b7"}, + {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:213bebd51821ed89b4d5b35bb10dbe67564228b3568f463a351a08e8b1677025"}, + {file = "PyQt5-5.15.4.tar.gz", hash = "sha256:2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be"}, +] +pyqt5-qt5 = [ + {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"}, +] +pyqt5-sip = [ + {file = "PyQt5_sip-12.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d5bca2fc222d58e8093ee8a81a6e3437067bb22bc3f86d06ec8be721e15e90a"}, + {file = "PyQt5_sip-12.9.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:d59af63120d1475b2bf94fe8062610720a9be1e8940ea146c7f42bb449d49067"}, + {file = "PyQt5_sip-12.9.0-cp310-cp310-win32.whl", hash = "sha256:0fc9aefacf502696710b36cdc9fa2a61487f55ee883dbcf2c2a6477e261546f7"}, + {file = "PyQt5_sip-12.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:485972daff2fb0311013f471998f8ec8262ea381bded244f9d14edaad5f54271"}, + {file = "PyQt5_sip-12.9.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a"}, + {file = "PyQt5_sip-12.9.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a"}, + {file = "PyQt5_sip-12.9.0-cp36-cp36m-win32.whl", hash = "sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197"}, + {file = "PyQt5_sip-12.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96"}, + {file = "PyQt5_sip-12.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206"}, + {file = "PyQt5_sip-12.9.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055"}, + {file = "PyQt5_sip-12.9.0-cp37-cp37m-win32.whl", hash = "sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f"}, + {file = "PyQt5_sip-12.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d"}, + {file = "PyQt5_sip-12.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644"}, + {file = "PyQt5_sip-12.9.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5"}, + {file = "PyQt5_sip-12.9.0-cp38-cp38-win32.whl", hash = "sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93"}, + {file = "PyQt5_sip-12.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7"}, + {file = "PyQt5_sip-12.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e"}, + {file = "PyQt5_sip-12.9.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850"}, + {file = "PyQt5_sip-12.9.0-cp39-cp39-win32.whl", hash = "sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121"}, + {file = "PyQt5_sip-12.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a"}, + {file = "PyQt5_sip-12.9.0.tar.gz", hash = "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32"}, +] pyrsistent = [ {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, diff --git a/dashboard/pyproject.toml b/dashboard/pyproject.toml index 49a5bff1..86f4f422 100644 --- a/dashboard/pyproject.toml +++ b/dashboard/pyproject.toml @@ -13,6 +13,9 @@ pip-chill = "^1.0.1" matplotlib = "^3.4.3" pywaffle = "^0.6.3" streamlit-autorefresh = "^0.0.1" +pandas = "^1.3.3" +requests = "^2.26.0" +PyQt5 = "^5.15.4" [tool.poetry.dev-dependencies] From 0ee677e7a71e812aed274b04ac5efa36f22108db Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 18:33:50 +0200 Subject: [PATCH 40/65] Removed pandas for less dependencies --- dashboard/backend/main.py | 7 +++---- dashboard/backend/requirements.txt | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py index c64f6bf3..486471d7 100644 --- a/dashboard/backend/main.py +++ b/dashboard/backend/main.py @@ -3,7 +3,6 @@ from pathlib import Path import sqlite3 from typing import Optional -import pandas as pd import uvicorn from fastapi import FastAPI from pydantic import BaseModel @@ -49,16 +48,16 @@ def get_leaderboard(hourrange=None, limit=2, count=True): addition = f" WHERE Date >= datetime('now','-{hourrange} hours')" agg = "count(*)" if count else "sum(Volume)" conn = sqlite3.connect(database_path) + cursor = conn.cursor() sql = f"SELECT Team, {agg} as amount FROM Team{addition} GROUP BY Team ORDER BY {agg} DESC LIMIT ?" - df = pd.read_sql(sql, conn, params=(limit,)) + cursor.execute(sql, (limit,)) + return_data = dict(cursor.fetchall()) conn.close() - return_data = dict(zip(df.Team.to_list(), df.amount.to_list())) return return_data @app.get("/leaderboard") def leaderboard(conf: BoardConfig): - print(conf.hourrange, conf.limit, conf.count) return get_leaderboard(conf.hourrange, conf.limit, conf.count) diff --git a/dashboard/backend/requirements.txt b/dashboard/backend/requirements.txt index 63873ac6..3c114fd0 100644 --- a/dashboard/backend/requirements.txt +++ b/dashboard/backend/requirements.txt @@ -1,3 +1,2 @@ fastapi==0.70.0 -uvicorn==0.15.0 -pandas \ No newline at end of file +uvicorn==0.15.0 \ No newline at end of file From 8b78a63a1be99922ee7f5b2a9fd7259f4334d0ce Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 19:45:43 +0200 Subject: [PATCH 41/65] Added update thread --- dashboard/qt-app/main.py | 1 - dashboard/qt-app/setup_leaderboard.py | 50 ++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/dashboard/qt-app/main.py b/dashboard/qt-app/main.py index 4ce2ae97..8c195549 100644 --- a/dashboard/qt-app/main.py +++ b/dashboard/qt-app/main.py @@ -8,5 +8,4 @@ app = QApplication(sys.argv) w = Leaderboard() w.showFullScreen() - # w.update(15) sys.exit(app.exec_()) diff --git a/dashboard/qt-app/setup_leaderboard.py b/dashboard/qt-app/setup_leaderboard.py index 956c1f0f..1e3d8e31 100644 --- a/dashboard/qt-app/setup_leaderboard.py +++ b/dashboard/qt-app/setup_leaderboard.py @@ -1,9 +1,11 @@ +import traceback +import sys from itertools import cycle import time import matplotlib from mainwindow import Ui_Leaderboard -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QRunnable, pyqtSlot, QObject, pyqtSignal, QThreadPool from PyQt5.QtWidgets import QMainWindow, qApp from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas @@ -28,15 +30,55 @@ def __init__(self): self.canvas = FigureCanvas(fig) self.horizontalLayout.addWidget(self.canvas) + # Spinning up a threadpool to constantly update + self.threadpool = QThreadPool() + self.start_worker() + def select(self): self.canvas.deleteLater() self.curr_option = next(self.options) self.canvas = FigureCanvas(generate_figure(self.curr_option)) self.horizontalLayout.addWidget(self.canvas) - def update(self, timing=15): + def update(self): self.canvas.deleteLater() self.canvas = FigureCanvas(generate_figure(self.curr_option)) self.horizontalLayout.addWidget(self.canvas) - time.sleep(timing) - qApp.processEvents() + self.start_worker() + + def start_worker(self): + """Starts a Worker with the timer, updates plot afterwards""" + worker = Worker(self.update_intervall) + worker.signals.finished.connect(self.update) + self.threadpool.start(worker) + + def update_intervall(self): + time.sleep(10) + + +class WorkerSignals(QObject): + finished = pyqtSignal() + error = pyqtSignal(tuple) + result = pyqtSignal(object) + + +class Worker(QRunnable): + def __init__(self, fn, *args, **kwargs): + super(Worker, self).__init__() + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + + @pyqtSlot() + def run(self): + try: + result = self.fn(*self.args, **self.kwargs) + except: + traceback.print_exc() + exctype, value = sys.exc_info()[:2] + self.signals.error.emit((exctype, value, traceback.format_exc())) + else: + self.signals.result.emit(result) + finally: + self.signals.finished.emit() From 7c1d9b4abefa74973742734e9edbb0863437159a Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 19:55:46 +0200 Subject: [PATCH 42/65] Switched to optimal python images --- dashboard/backend/Dockerfile | 2 +- dashboard/frontend/Dockerfile | 2 +- docker-compose.yml | 1 - microservice/Dockerfile | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dashboard/backend/Dockerfile b/dashboard/backend/Dockerfile index e85ead33..5ffbad19 100644 --- a/dashboard/backend/Dockerfile +++ b/dashboard/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.8-slim-buster WORKDIR /app diff --git a/dashboard/frontend/Dockerfile b/dashboard/frontend/Dockerfile index b2d3c60f..b8bece8e 100644 --- a/dashboard/frontend/Dockerfile +++ b/dashboard/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-buster +FROM python:3.9-slim-buster WORKDIR /app diff --git a/docker-compose.yml b/docker-compose.yml index dcb53572..47f7b0cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,6 @@ services: container_name: cocktail-microservice restart: always build: ./microservice/ - image: andrewo92/coktail-microservice:latest volumes: - ./microservice/:/usr/src/app/ ports: diff --git a/microservice/Dockerfile b/microservice/Dockerfile index 8263d2c8..6cd896ca 100644 --- a/microservice/Dockerfile +++ b/microservice/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8 +FROM python:3.8-slim-buster RUN mkdir /usr/src/app/ COPY . /usr/src/app/ WORKDIR /usr/src/app/ From c731426ebb16d8dcbb8600ec0dc8ffd96310b8f5 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 21:16:44 +0200 Subject: [PATCH 43/65] Fixed figure memory leak --- dashboard/qt-app/waffle.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/dashboard/qt-app/waffle.py b/dashboard/qt-app/waffle.py index 92267c4e..a8dddd9c 100644 --- a/dashboard/qt-app/waffle.py +++ b/dashboard/qt-app/waffle.py @@ -67,17 +67,11 @@ def get_data(count: bool, hourrange: int, limit: int): def decide_data(datatype: int): - count = True - sort = True - hourrange = None - if datatype in (1, 2): - hourrange = 24 - limit = 5 - if datatype in (3, 4): - sort = False - limit = 10 - if datatype in (2, 4): - count = False + index = datatype - 1 + count = [True, False, True, False][index] + sort = [True, True, False, False][index] + hourrange = [24, 24, None, None][index] + limit = [5, 5, 10, 10][index] return (count, sort, hourrange, limit) @@ -93,6 +87,9 @@ def generate_figure(datatype: int): data = get_data(count, hourrange, limit) waffle_data = extract_data(sort, data) dims = generate_dimensions(sum(data.values()), count) + # close current (old) figure, to avoid memory leak + # this is needed because old figures will keep open until explicitly closed + plt.close() fig = plt.figure( FigureClass=Waffle, **dims, From 5587834cd43ee1a4e77925fa2c4f3270d8a9ce7c Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 21:17:04 +0200 Subject: [PATCH 44/65] adjusted requirements of main programm --- requirements.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 87371f2b..c811f4c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -PyQt5==5.13.2 -PyQt5-sip==12.7.0 -requests==2.25.1 \ No newline at end of file +PyQt5 +requests \ No newline at end of file From 143de2ff20554f9e9b572f548f38f925011a0337 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 17 Oct 2021 21:17:50 +0200 Subject: [PATCH 45/65] Added reqs to qt-dashboard --- dashboard/qt-app/requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 dashboard/qt-app/requirements.txt diff --git a/dashboard/qt-app/requirements.txt b/dashboard/qt-app/requirements.txt new file mode 100644 index 00000000..6acb768a --- /dev/null +++ b/dashboard/qt-app/requirements.txt @@ -0,0 +1,4 @@ +matplotlib +pywaffle +requests +PyQt5 \ No newline at end of file From 0e927759ae0f7736d70204b07ce2185a7a21a5c6 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 18 Oct 2021 13:38:42 +0200 Subject: [PATCH 46/65] Adjusted BG color --- dashboard/qt-app/mainwindow.py | 2 +- dashboard/qt-app/mainwindow.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/qt-app/mainwindow.py b/dashboard/qt-app/mainwindow.py index 6ae9d3c3..bec4a2b1 100644 --- a/dashboard/qt-app/mainwindow.py +++ b/dashboard/qt-app/mainwindow.py @@ -26,7 +26,7 @@ def setupUi(self, Leaderboard): Leaderboard.setStyleSheet("QWidget\n" "{\n" " color: rgb(0, 123, 255); \n" -" background-color: rgb(0, 0, 0);\n" +" background-color: rgb(14, 17, 23);\n" "\n" "}\n" "\n" diff --git a/dashboard/qt-app/mainwindow.ui b/dashboard/qt-app/mainwindow.ui index a3b4c2f6..df30849a 100644 --- a/dashboard/qt-app/mainwindow.ui +++ b/dashboard/qt-app/mainwindow.ui @@ -38,7 +38,7 @@ QWidget { color: rgb(0, 123, 255); - background-color: rgb(0, 0, 0); + background-color: rgb(14, 17, 23); } From 0537f350f5251fbd723c4d787409e012528dd030 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 18 Oct 2021 13:39:10 +0200 Subject: [PATCH 47/65] Added team post to maker --- src/maker.py | 4 ++++ src/service_handler.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/maker.py b/src/maker.py index 34192a09..ed706c38 100644 --- a/src/maker.py +++ b/src/maker.py @@ -158,6 +158,10 @@ def prepare_cocktail(w): print("Verbrauchsmengen: ", consumption) SERVICE_HANDLER.post_cocktail_to_hook(cocktailname, cocktail_volume) + # only post team if cocktail was made over 60% + readiness = taken_time / max_time + if readiness >= 0.6: + SERVICE_HANDLER.post_team_data(shared.selected_team, round(cocktail_volume * readiness)) if shared.make_cocktail: DB_COMMANDER.set_multiple_ingredient_consumption([x[0] for x in update_data], [x[1] for x in update_data]) diff --git a/src/service_handler.py b/src/service_handler.py index 1b85d9c0..848050f8 100644 --- a/src/service_handler.py +++ b/src/service_handler.py @@ -33,11 +33,11 @@ def send_mail(self, file_name: str, binary_file) -> Dict: files = {"upload_file": (file_name, binary_file,)} return self.try_to_send(endpoint, post_type="file", files=files) - def post_team_data(self, team_name: str) -> Dict: + def post_team_data(self, team_name: str, cocktail_volume: int) -> Dict: """Post the given team name to the team api if activated""" if not self.USE_TEAMS: return team_disabled() - payload = json.dumps({"team": team_name}) + payload = json.dumps({"team": team_name, "volume": cocktail_volume}) endpoint = f"{self.TEAM_API_URL}/cocktail" return self.try_to_send(endpoint, payload=payload, post_type="teamdata") From 3c847c4ed9c4b964a49325ae505cc0ec928956c8 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 18 Oct 2021 13:39:19 +0200 Subject: [PATCH 48/65] Added typehinty --- src/database_commander.py | 93 ++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/src/database_commander.py b/src/database_commander.py index ddaccbba..3275b872 100644 --- a/src/database_commander.py +++ b/src/database_commander.py @@ -2,6 +2,7 @@ import os from pathlib import Path import sqlite3 +from typing import Any DATABASE_NAME = "Cocktail_database" DIRPATH = os.path.dirname(__file__) @@ -13,40 +14,40 @@ class DatabaseCommander: def __init__(self): self.handler = DatabaseHandler() - def get_recipe_id_by_name(self, recipe_name): + def get_recipe_id_by_name(self, recipe_name: str) -> int: query = "SELECT ID FROM Rezepte WHERE Name=?" value = self.handler.query_database(query, (recipe_name,)) if not value: return 0 return value[0][0] - def get_recipe_ingredients_by_id(self, recipe_id): + def get_recipe_ingredients_by_id(self, recipe_id: int): query = """SELECT Zutaten.Name, Zusammen.Menge, Zusammen.Hand, Zutaten.ID FROM Zusammen INNER JOIN Zutaten ON Zusammen.Zutaten_ID = Zutaten.ID WHERE Zusammen.Rezept_ID = ?""" return self.handler.query_database(query, (recipe_id,)) - def get_recipe_ingredients_by_name(self, recipe_name): + def get_recipe_ingredients_by_name(self, recipe_name: str): query = """SELECT Zutaten.Name, Zusammen.Menge, Zusammen.Hand, Zutaten.Alkoholgehalt FROM Zusammen INNER JOIN Zutaten ON Zutaten.ID = Zusammen.Zutaten_ID WHERE Zusammen.Rezept_ID = (SELECT ID FROM Rezepte WHERE Name = ?)""" return self.handler.query_database(query, (recipe_name,)) - def get_recipe_ingredients_with_bottles(self, recipe_name): + def get_recipe_ingredients_with_bottles(self, recipe_name: str): query = """SELECT Zutaten.Name, Zusammen.Menge, Belegung.Flasche, Zusammen.Alkoholisch, Zutaten.Mengenlevel FROM Zusammen LEFT JOIN Belegung ON Zusammen.Zutaten_ID = Belegung.ID INNER JOIN Zutaten ON Zutaten.ID = Zusammen.Zutaten_ID WHERE Zusammen.Rezept_ID = (SELECT ID FROM Rezepte WHERE Name =?)""" return self.handler.query_database(query, (recipe_name,)) - def get_recipe_handadd_window_properties(self, recipe_name): + def get_recipe_handadd_window_properties(self, recipe_name: str): query = """SELECT Z.Zutaten_ID, Z.Menge, Z.Alkoholisch FROM Zusammen AS Z INNER JOIN Rezepte AS R ON R.ID = Z.Rezept_ID WHERE R.Name = ? AND Z.Hand = 1""" return self.handler.query_database(query, (recipe_name,)) - def get_recipe_ingredients_by_name_seperated_data(self, recipe_name): + def get_recipe_ingredients_by_name_seperated_data(self, recipe_name: str): data = self.get_recipe_ingredients_by_name(recipe_name) handadd_data = [] machineaddd_data = [] @@ -61,7 +62,7 @@ def get_all_recipes_properties(self): query = "SELECT ID, Name, Alkoholgehalt, Menge, Kommentar, Enabled FROM Rezepte" return self.handler.query_database(query) - def build_recipe_object(self): + def build_recipe_object(self) -> dict[str, dict]: recipe_object = {} recipe_data = self.get_all_recipes_properties() for recipe in recipe_data: @@ -76,7 +77,7 @@ def build_recipe_object(self): } return recipe_object - def get_recipe_ingredients_for_comment(self, recipe_name): + def get_recipe_ingredients_for_comment(self, recipe_name: str): query = """SELECT Zutaten.Name, Zusammen.Menge, Zutaten.ID, Zusammen.Alkoholisch, Zutaten.Alkoholgehalt FROM Zusammen INNER JOIN Rezepte ON Rezepte.ID=Zusammen.Rezept_ID @@ -84,53 +85,53 @@ def get_recipe_ingredients_for_comment(self, recipe_name): WHERE Rezepte.Name = ? AND Zusammen.Hand = 1""" return self.handler.query_database(query, (recipe_name,)) - def get_enabled_recipes_id(self): + def get_enabled_recipes_id(self) -> list[int]: recipe_data = self.get_all_recipes_properties() return [x[0] for x in recipe_data if x[5]] - def get_disabled_recipes_id(self): + def get_disabled_recipes_id(self) -> list[int]: recipe_data = self.get_all_recipes_properties() return [x[0] for x in recipe_data if not x[5]] - def get_recipes_name(self): + def get_recipes_name(self) -> list[str]: recipe_data = self.get_all_recipes_properties() return [x[1] for x in recipe_data] - def get_ingredients_at_bottles(self): + def get_ingredients_at_bottles(self) -> list[str]: query = "SELECT Zutat_F FROM Belegung" result = self.handler.query_database(query) return [x[0] for x in result] - def get_ingredients_at_bottles_without_empty_ones(self): + def get_ingredients_at_bottles_without_empty_ones(self) -> list[str]: data = self.get_ingredients_at_bottles() return [x for x in data if x != ""] - def get_ids_at_bottles(self): + def get_ids_at_bottles(self) -> list[int]: query = "SELECT ID FROM Belegung" result = self.handler.query_database(query) return [x[0] for x in result] - def get_ingredient_names(self, condition_filter=""): + def get_ingredient_names(self, condition_filter="") -> list[str]: query = "SELECT Name FROM Zutaten" if condition_filter != "": query = f"{query} {condition_filter}" names = self.handler.query_database(query) return [x[0] for x in names] - def get_ingredient_names_hand(self): + def get_ingredient_names_hand(self) -> list[str]: return self.get_ingredient_names("WHERE Hand = 1") - def get_ingredient_names_machine(self): + def get_ingredient_names_machine(self) -> list[str]: return self.get_ingredient_names("WHERE Hand = 0") - def get_ingredient_name_from_id(self, ingredient_id): + def get_ingredient_name_from_id(self, ingredient_id: int) -> str: query = "SELECT Name FROM Zutaten WHERE ID = ?" data = self.handler.query_database(query, (ingredient_id,)) if data: return data[0][0] return "" - def get_bottle_fill_levels(self): + def get_bottle_fill_levels(self) -> list[int]: query = """SELECT Zutaten.Mengenlevel, Zutaten.Flaschenvolumen FROM Belegung LEFT JOIN Zutaten ON Zutaten.ID = Belegung.ID""" values = self.handler.query_database(query) @@ -138,12 +139,12 @@ def get_bottle_fill_levels(self): for current_value, max_value in values: # restrict the value between 0 and 100 proportion = 0 - if current_value != None: + if current_value is not None: proportion = round(min(max(current_value / max_value * 100, 0), 100)) levels.append(proportion) return levels - def get_ingredient_data(self, ingredient_name): + def get_ingredient_data(self, ingredient_name: str) -> dict[str, Any]: query = "SELECT ID, Name, Alkoholgehalt, Flaschenvolumen, Hand, Mengenlevel FROM Zutaten WHERE Name = ?" values = self.handler.query_database(query, (ingredient_name,)) if values: @@ -158,25 +159,25 @@ def get_ingredient_data(self, ingredient_name): } return {} - def get_bottle_usage(self, ingredient_id): + def get_bottle_usage(self, ingredient_id: int): query = "SELECT COUNT(*) FROM Belegung WHERE ID = ?" if self.handler.query_database(query, (ingredient_id,))[0][0]: return True return False - def get_recipe_usage_list(self, ingredient_id): + def get_recipe_usage_list(self, ingredient_id: int) -> list[str]: query = """SELECT Rezepte.Name FROM Zusammen INNER JOIN Rezepte ON Rezepte.ID = Zusammen.Rezept_ID WHERE Zusammen.Zutaten_ID=?""" recipe_list = self.handler.query_database(query, (ingredient_id,)) return [recipe[0] for recipe in recipe_list] - def get_handadd_ids(self): + def get_handadd_ids(self) -> list[int]: query = "SELECT ID FROM Vorhanden" result = self.handler.query_database(query) return [x[0] for x in result] - def get_ingredients_seperated_by_handadd(self, recipe_id): + def get_ingredients_seperated_by_handadd(self, recipe_id: int): handadds = [] machineadds = [] ingredients = self.get_recipe_ingredients_by_id(recipe_id) @@ -187,13 +188,13 @@ def get_ingredients_seperated_by_handadd(self, recipe_id): machineadds.append(ingredient[3]) return handadds, machineadds - def get_multiple_recipe_names_from_ids(self, id_list): + def get_multiple_recipe_names_from_ids(self, id_list: list[int]) -> list[str]: questionmarks = ",".join(["?"] * len(id_list)) query = f"SELECT Name FROM Rezepte WHERE ID in ({questionmarks})" result = self.handler.query_database(query, id_list) return [x[0] for x in result] - def get_multiple_ingredient_ids_from_names(self, name_list): + def get_multiple_ingredient_ids_from_names(self, name_list: list[str]) -> list[int]: questionmarks = ",".join(["?"] * len(name_list)) query = f"SELECT ID FROM Zutaten WHERE Name in ({questionmarks})" result = self.handler.query_database(query, name_list) @@ -209,17 +210,17 @@ def get_consumption_data_lists_ingredients(self): data = self.handler.query_database(query) return self.convert_consumption_data(data) - def convert_consumption_data(self, data): + def convert_consumption_data(self, data: list[list]): headers = [row[0] for row in data] resetable = [row[1] for row in data] lifetime = [row[2] for row in data] return [["date", *headers], [datetime.date.today(), *resetable], ["lifetime", *lifetime]] - def get_enabled_status(self, recipe_name): + def get_enabled_status(self, recipe_name: str) -> int: query = "SELECT Enabled FROM Rezepte WHERE Name = ?" return self.handler.query_database(query, (recipe_name,))[0] - def get_available_ingredient_names(self): + def get_available_ingredient_names(self) -> list[str]: query = """SELECT Zutaten.Name FROM Zutaten INNER JOIN Vorhanden ON Vorhanden.ID = Zutaten.ID""" data = self.handler.query_database(query) @@ -240,7 +241,7 @@ def get_ingredient_bottle_and_level_by_name(self, ingredient_name): return 0, 0 # set (update) commands - def set_bottleorder(self, ingredient_names): + def set_bottleorder(self, ingredient_names: list[str]): for i, ingredient in enumerate(ingredient_names): bottle = i + 1 query = """UPDATE OR IGNORE Belegung @@ -250,7 +251,7 @@ def set_bottleorder(self, ingredient_names): searchtuple = (ingredient, ingredient, bottle) self.handler.query_database(query, searchtuple) - def set_bottle_volumelevel_to_max(self, boolean_list): + def set_bottle_volumelevel_to_max(self, boolean_list: list[bool]): query = """UPDATE OR IGNORE Zutaten Set Mengenlevel = Flaschenvolumen WHERE ID = (SELECT ID FROM Belegung WHERE Flasche = ?)""" @@ -258,7 +259,7 @@ def set_bottle_volumelevel_to_max(self, boolean_list): if set_to_max: self.handler.query_database(query, (bottle,)) - def set_ingredient_data(self, ingredient_name, alcohollevel, volume, new_level, onlyhand, ingredient_id): + def set_ingredient_data(self, ingredient_name: str, alcohollevel: int, volume: int, new_level: int, onlyhand: int, ingredient_id: int): query = """UPDATE OR IGNORE Zutaten SET Name = ?, Alkoholgehalt = ?, Flaschenvolumen = ?, @@ -268,14 +269,14 @@ def set_ingredient_data(self, ingredient_name, alcohollevel, volume, new_level, searchtuple = (ingredient_name, alcohollevel, volume, new_level, onlyhand, ingredient_id) self.handler.query_database(query, searchtuple) - def set_recipe_counter(self, recipe_name): + def set_recipe_counter(self, recipe_name: str): query = """UPDATE OR IGNORE Rezepte SET Anzahl_Lifetime = Anzahl_Lifetime + 1, Anzahl = Anzahl + 1 WHERE Name = ?""" self.handler.query_database(query, (recipe_name,)) - def set_ingredient_consumption(self, ingredient_name, ingredient_consumption): + def set_ingredient_consumption(self, ingredient_name: str, ingredient_consumption: int): query = """UPDATE OR IGNORE Zutaten SET Verbrauchsmenge = Verbrauchsmenge + ?, Verbrauch = Verbrauch + ?, @@ -284,7 +285,7 @@ def set_ingredient_consumption(self, ingredient_name, ingredient_consumption): searchtuple = (ingredient_consumption, ingredient_consumption, ingredient_consumption, ingredient_name) self.handler.query_database(query, searchtuple) - def set_multiple_ingredient_consumption(self, ingredient_name_list, ingredient_consumption_list): + def set_multiple_ingredient_consumption(self, ingredient_name_list: list[str], ingredient_consumption_list: list[int]): for ingredient_name, ingredient_consumption in zip(ingredient_name_list, ingredient_consumption_list): self.set_ingredient_consumption(ingredient_name, ingredient_consumption) @@ -292,45 +293,45 @@ def set_all_recipes_enabled(self): query = "UPDATE OR IGNORE Rezepte SET Enabled = 1" self.handler.query_database(query) - def set_recipe(self, recipe_id, name, alcohollevel, volume, comment, enabled): + def set_recipe(self, recipe_id: int, name: str, alcohollevel: int, volume: int, comment: str, enabled: int): query = """UPDATE OR IGNORE Rezepte SET Name = ?, Alkoholgehalt = ?, Menge = ?, Kommentar = ?, Enabled = ? WHERE ID = ?""" searchtuple = (name, alcohollevel, volume, comment, enabled, recipe_id) self.handler.query_database(query, searchtuple) - def set_ingredient_level_to_value(self, ingredient_id, value): + def set_ingredient_level_to_value(self, ingredient_id: int, value: int): query = "UPDATE OR IGNORE Zutaten SET Mengenlevel = ? WHERE ID = ?" self.handler.query_database(query, (value, ingredient_id)) # insert commands - def insert_new_ingredient(self, ingredient_name, alcohollevel, volume, onlyhand): + def insert_new_ingredient(self, ingredient_name: str, alcohollevel: int, volume: int, onlyhand: int): query = """INSERT OR IGNORE INTO Zutaten(Name,Alkoholgehalt,Flaschenvolumen,Verbrauchsmenge,Verbrauch,Mengenlevel,Hand) VALUES (?,?,?,0,0,0,?)""" searchtuple = (ingredient_name, alcohollevel, volume, onlyhand) self.handler.query_database(query, searchtuple) - def insert_new_recipe(self, name, alcohollevel, volume, comment, enabled): + def insert_new_recipe(self, name: str, alcohollevel: int, volume: int, comment: str, enabled: int): query = """INSERT OR IGNORE INTO Rezepte(Name, Alkoholgehalt, Menge, Kommentar, Anzahl_Lifetime, Anzahl, Enabled) VALUES (?,?,?,?,0,0,?)""" searchtuple = (name, alcohollevel, volume, comment, enabled) self.handler.query_database(query, searchtuple) - def insert_recipe_data(self, recipe_id, ingredient_id, ingredient_volume, isalcoholic, hand_add): + def insert_recipe_data(self, recipe_id: int, ingredient_id: int, ingredient_volume: int, isalcoholic: int, hand_add: int): query = "INSERT OR IGNORE INTO Zusammen(Rezept_ID, Zutaten_ID, Menge, Alkoholisch, Hand) VALUES (?, ?, ?, ?, ?)" searchtuple = (recipe_id, ingredient_id, ingredient_volume, isalcoholic, hand_add) self.handler.query_database(query, searchtuple) - def insert_multiple_existing_handadd_ingredients_by_name(self, ingredient_names): + def insert_multiple_existing_handadd_ingredients_by_name(self, ingredient_names: list[str]): ingredient_id = self.get_multiple_ingredient_ids_from_names(ingredient_names) questionmarks = ",".join(["(?)"] * len(ingredient_id)) query = f"INSERT INTO Vorhanden(ID) VALUES {questionmarks}" self.handler.query_database(query, ingredient_id) # delete - def delete_ingredient(self, ingredient_id): + def delete_ingredient(self, ingredient_id: int): query = "DELETE FROM Zutaten WHERE ID = ?" self.handler.query_database(query, (ingredient_id,)) @@ -342,13 +343,13 @@ def delete_consumption_ingredients(self): query = "UPDATE OR IGNORE Zutaten SET Verbrauch = 0" self.handler.query_database(query) - def delete_recipe(self, recipe_name): + def delete_recipe(self, recipe_name: str): query1 = "DELETE FROM Zusammen WHERE Rezept_ID = (SELECT ID FROM Rezepte WHERE Name = ?)" query2 = "DELETE FROM Rezepte WHERE Name = ?" self.handler.query_database(query1, (recipe_name,)) self.handler.query_database(query2, (recipe_name,)) - def delete_recipe_ingredient_data(self, recipe_id): + def delete_recipe_ingredient_data(self, recipe_id: int): query = "DELETE FROM Zusammen WHERE Rezept_ID = ?" self.handler.query_database(query, (recipe_id,)) @@ -374,7 +375,7 @@ def connect_database(self): self.database = sqlite3.connect(self.database_path) self.cursor = self.database.cursor() - def query_database(self, sql, serachtuple=()): + def query_database(self, sql: str, serachtuple=()): self.connect_database() self.cursor.execute(sql, serachtuple) From f4d24c843a8c90ee572eb00da239e23f7607cc65 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 18 Oct 2021 13:39:45 +0200 Subject: [PATCH 49/65] Added docs for v1.2 --- docs/pictures/dashboard.png | Bin 0 -> 66954 bytes docs/pictures/teams_ui.png | Bin 0 -> 14980 bytes readme.md | 73 ++++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 docs/pictures/dashboard.png create mode 100644 docs/pictures/teams_ui.png diff --git a/docs/pictures/dashboard.png b/docs/pictures/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..52b9c292fb676493e1e41ee334989c2cb42c604c GIT binary patch literal 66954 zcmeEv30PBCxAp;49HG_E5s|jqT17x>1!aoXR@z#nR%MVO4i%~l5fEYsNw8L_rAk`` zv=;2_%G&;pF`L1VP&SRk{CvpZnkMyYM`Vl~d1kL#D(~rM`AoSOX7bi~w|N8}6AO`<4iTcXpBPg$S zVJG-7A$Y@<4G>h2G)=!}BKSPz(5Jpp5M)t-{%2B&yYe*zA(@|jykWyuWY9*>^6!p94pEI#Mexuw?62D>IscI|^;&g^Q%RDcvOAp&KlJDVrK1M!1OF z(q3Q!x-<0Sk_f#q{5ur$mdRTqAG+6@t{?f(cyvPi$cKEhDbPp@yUk}pro&&ATP%Sl z41b8X{uG)#`~h-#P-0eo%wk;*`>D0OMJOk$rHcm3&0{JT5ks#Ng;J5Q2kEyE7A6gz ziCi>tSH44zEmV4Y8&kHZH{Sg0Z~i6wPOrPnR+q`^0y-biQWzush+mbv4sum=FfZJ) zLIy5dzJ)t-DJ^C7NY~;Ea+X1*H*X}wsJRGS{Z{x7TKYuy)^giq#>;C~q1Uu;W7=;l zzFLC7HV=f)E;~5{qq8^kv(INcbfkf8@ST5iOH;ZmE;Z6?_$z4Ti8T}B)k+oDwgrst zuXp)OJhafS7FC&{pF3|spV!nXx-ofum&v@UJUVK%0$y0^|NgITJgRugx#*R{>(Ei%xA zq}p|o7Fo78cPqgSJR-kvLi~(uUo^gZdZa!o1M{`HYMs1PSz>1Co>My4@grz0$9Cc2 zZ`5^ELn!(zidMo*ZJ^)ISqyzw$m(vll+Bq-an+}?b*1t@0@_ueq4N5fkmHpgG@SeK zsW}kb+w0De7U{e_SICc_dILHw-|v91+nhus`{LGbWxG85xus;gAZwGih`&Z_DM@_Q zcEzownCNqN6j9Pk@-G7|K|`U|CcBRvM5Dm+dUi#+%<+#&G^#^fTVgw$|E6hqgbJ%9 z$ygE#Zl6q9i!aM6CFj0qaYB4*&wv2=NhZM{nw38bC1Q-G^M!aP4~w@*BoG;D-(J(G zW8FA)dIiR;elxT-uK|XwPKaYPtRdNYF-}82xz#wEmLtjwlCneR?kb{E*+Rz96fYQ_ z;`rR?@}m?V&g{^70{` zBElY-dKn#?cu04adbQ{SvOOz=%vyYT)Uc!}AfEYpD5%e}1;JN@8Le_oc=+ ze-RMg{a9Kf1>sq;#q3+;b~ouN>13#>4$N>%Plt!K+ZX#an&0O`t^6cnX`bh60 zv4$$|gnKJ64eT3Rs*NM9d3R)_&gdH2=#{U(5IxW&+;5Nn5`S}bhMPXQUGnWT`{#La z{E!stl#q9!mRPtU8X2raT=XeyPb7FSzXG{Z8eBFq@6~RyKFZlU2VSu?A8>~;4$1!!iT=I3|9Y%&g*t*Fhg<25k#A@0mERz%OKhGBL&$VzJ%$F@v zmtfUQPfCzF3F#k@MNzRnS~aYsBBd^fUdhHH+6LkL;0)G>^d!oYGs2Tfx^$8Lfyaun zQ`y>5FRK|)Qfd1`p)#=dciCbaH^QZ|bc-T^F}!72aiv7Vw7?djqMm6h#}%A0 zEk8y-Jot#);O2>V@oKbMu_))vLMYHM)w^33#Wjy{pxmaFBxUFeGBK-zFGSpVR7&Iy z-gYRVlVB@ho|=+RY!ydUO_*9E%qbBz8S#SM!`{I+2BhYk9+{MDSt$4pag7hK6kSA`3?P)CjLrQa*KAA4#fDt8Qm$6dn%=eKFKQi~^ z^BS+HCdF4SQa{LCAM7Tv61x@>%gt=AKVdi?Tskox_A_KF&-2K6rQ}ON>9?TXD}ojd zkw^60HT70Kk)z7R;VUs6eZqGgqSkAgqG*#5w|= z-Jr%L(ze=68o=FbR%+*j%z)@bIPEZY@aEhEE~7x%+)&10-^x^D2UsD6>1O4d<@nY| zY4;401k6HNb7>uSO>eAI@c}SVA-NF`lj*TFnM`frg)>`Yk+1H@_EEWSx-5gb#k;)H z=a%-q+pe-&BV7W? zFMOfqk;f%Iq5EmZ3+G}nY7WiLcIhR-?tXz{t{twP@C@sIxit6^aA~6UaJByr4vtUb zT94T@Kk#-Amq5Li^f*;pSMEAkz*{sZv0O$Oh|PJ|zKgr*a715a1vbouis&h8%xNy~ zLfza&DsQfq-z%`g;7h87)kMdG^`A|FZ2GP|b?U#v}f#oi7i%caKk#8hS*(m)~wvnZ&HU^?_b`F2*6i80y>1J@76iO);iLKk0>AUo8Fd%oLEpJl{?~@7fA(oJw zazA1-(u3o7a^HbJNOifQZ?e-qC~<}bc!%gF+p}t*GEmP5+(?tw;NYyc-}WJEugJ=e zS=!1w%X&X?u+H-*oGuofsvExee8EU0DFx)TSvF-DZ5dz8v4?Js0f;9K0z({8fg zgvV^O-^=cK5}Lp2jlMHKc)E9e7UF!@uqBEwUtPU*2E=pMrEY%MAvG9gY{pI$Qj(uV zSdx9z@#gN|bcUVwJWq^Odm{P)Sz10Av{Jnbn4(PXG$ z4kS_=D)TyRDLRTb?#R=$)R@)^8$ulZet~6Ra}taUsBo4&2tKG#$vG2~jN#t?&6{v+ys{gc!b(Hp)(t`qI{Cqx5V#`|n(8J8_n;l3i*e?mrnNtGJ9=+u_< zh>r6W0id&v)rOKTLs`=@lU9IxPKZM_1lUTnBT&4zzUiLHZp861SQoROk>JZapfbrMv5MWMO>_YPx%zRuKgNMt~7!UY|9xk}?c4aE#hrq8}V>_iFl* z5T@?f<^T@B7hcn1?a5@t8ET%(uPm@C%MMOBQfAe1y%e_sZrD*9TfGChTtF17&nY-= zDlmfRRM@J|@hP?QvWF;JSBvMk=-&eazr*O_l=<4(mkf4Z;$zsAq-x%|OeEb~r)@RZ zM$&WxK}!L2Exm8Dnk(y%NTPg2@J#d#&YBbtQ_u4`_5<6ghNMi)K7JX^x5q9Ut5tZ{ zmu;LFPm@;2!9==Lc=`~?hno)wYm#J3_V%=&V3c`|HzREu590`8SL$>=ArJ= z2w%ThIs(94VLw^665#zD)Ji+ZS;YqF8^P(smXLiL_!+JxqboQ{9FHe?muE!PaKgQALKT*$-;uPNBcMw~MP) zJ>Je;6W6Rzf^-_aG?*43b*6lv3|XXO)Kf5iJ&w$>r@dCWt`z-JCwsXsIy7@k|C{Y) zA#0>PzNApr&rC1bcTn$}NzNcz^UAQbF6w)JAf+ePapqp3glxG`7D&*p%ufoT>FS@N z2x;(EyTnhdjUm7*Ov{_JPzh4i-*thk?Xmb8X4foq-rBcQE4kseKORE~Jern?s zr>Lr%xe86OF-BrW$I8vvfvUIX`w+DB&AGCxXnII1H6 zFYOz=-VS`H(9A+#p~%RMd@!4St*b#gfK?6>Qyb}}cr3^wVm{OdRrAgmf&j=^L!1U- z1|4kd<$g&9>B-=9E74#_yBn=(ib<;y+xw)Axr?mDT;i*8i=<7TLW3F}(r`$vs7v5c z4(Cc!)H&XWV~sB^$weE3UoO8o84C96p~z1Ydn>Al^)zuc7P;e;;2A*907xlW9r`h% zk}zsxxsz6A;Q!6~RAu1L4#ouMkOgQ@hf18Bj3 zKNV8sv9`haiMC>Co-6F_;-wBf4xqBrud1P99R?-%l)Gw@oA%c33JH&P#l2kE#aR~dawd^pn?Mx~aH7@O{*(SSt zK%8CKdlevn-^Kj?qkTE_+nDZN%yPLe9YyfHCvAD#J2C=AW?;syk3^u?rQ%wzbi=7w zZ%)^mYND4idlq&zSlv;|OU;~L4i!TANSCN9!3e%@)2ri~16GccQnB#Jsj)uo-I>!IAe@3ej3hIceCV?ul(=-{ok?Wt6<2=>nN zR$m`P?U4Svc8Q^Kp?4tnO-5}sR?}r<5-y$^cC{C`Aahv0zumiM;{xUv7aqyDbOKmn zLz@b>Lhefg6NUmbFkyXd6!>}X)oR}6K{}T5M2F>c@%PGF7u?|E_TbeU4}iJd?KH9) z)3}5#vBlN$D&@rl*V58pJC3%HZb`kSzd)$>HOBG;-maWEQ23Iy=l9q9wG}yEDb~5k z8{h#Z*BW#ws^yrCmxwvvqO597iHDoEK!|~r0`_)K*b8saNGvhYQMCY|R4XL4*LL|| zKfmEin`ke|Vw(WO`*ZnsKy_C8q^me@;&Ox1$zFU_+JQn+K?X(SXG2P8%S#TX8NMKQ zpQTulZz`0jl^9^fJWb1!wO0sUE0{k!k@G%5>KnUCVQV=$NJDNuzY|Z0Mp?24n}+?y zgAS>9u;ZtxD=@)SxLS5C6;lj`nelub`RAp z#_?fAs$@FmKzma*-G);aAQQn-kF9~)DoL&@VCj0QsF>=55f5hoKc>hy_59eCt~mWH z>I%AXc?OsDDWfUeH4)Mr1_+T8+@3r%!Y4^lNftk6vSuBRxQ74(wY3d+`fG!=dqg`y zu|2~}RM)jC36{1kU4cP>?MVf}UeZ>VFfe(NKhB7-Lzrh|ZHBhkg+~kCp5;T>C7x8z z-4b4(*6ldcE8uGY#7jU}IWayr>U<`WEwn)fDGAhT3L~*14~am7!)1CaX^xtf=$)9T z{VA&?crmz~w0>s*Sa=JeaK6AUI6eIzkUJoQtYyt=qm|TKtAPW*iu^j*6Nv`l$=s)# zF2poHL6?PYYxB)p3fbE{gdGDyK$yAfzl*4iU7S&KIx*<{F6hXe^T8HezCu|~iSr|nXuiQ9fTpOs zG;Mh#inc5OJ`Hp~?=VS1^!Lt+(i;5|F26rd=%yP=O_<4$Vt6wfR~j(RVb3$Iv-`%{Fz!BjW> zcYUX!d`@s^Vj^Y{Fu@X`oPZtf7SgMAvcUHMtU-T!E|hn@8F}v%iQIb(v8Pm?S__z3 z)JPU}CiT42~s?8`+;TF+qhr&{@0>Nx^0IP=1c5p5iIMGZL7F8^j=@{n` znCFZdn<4hk@nyHq&rbM}Ad71tp5+P-{$g4#ag`sh$jWmyuC`51tWOBoM8JM%Dm<@v z+AIl_Pja%82PBHQ&)QFT?jlTxSGn!eJvE)$Sdn% zuQLDvZ)nU>qNGA+E|E{Nvj@JXq|OsVLc?-E@rtd9EgP!2Yr4nKbL2hSS6ai=k40vR&f3su@&U?}hp8wvcSb>{AH8mFk5(DIbJ!;rz~?uUSqm--qLAk-p7>GRtTwsOIA2ckwi z1G8V#DV{oLo>~$|;KibhnU#C=iyi_f*5B)w>xyb)yPKU+d8H;IbA?)7nrfc@_ zG&G{LZ5nHzq5m` zd!HEC;2n*IZ@|tZVBuz9x_MYk9}XCZLOfSOs=yWN?{hok0O&ki;ZM%0y&pc>= z11j(=sU`Xg*QI74=OmjDt|MuD|kTAFojyIFa)^`X08b+47ib=4sc1G=txzb_ZAFf#GfB7JCm12W*Qcz6cj zhoM!3#(wfz7kO!$lOH*w_m8Zz>g(sCW4?kKbq^0@&A{7ZYzA8ss-)^dCr+mwI)GAT zfP5KnoRlN5721sBY6$pJM~5#+0Me`jC?X2*GokmqcjF3CqPPNcL&4a7Y!etph}F|0ch&!qJcO;YuFWzlN0)pMheXGSwe?K)F$-2C zK^~-);kizF5I${7B8L*-*Z@s-LpK?$)afrKW8gTCEK^qCiU8qnhxiy-h65(8IgPo< z=W;X}06P7l)RxNy> zVi;^&Xv+Rs883X+2#t^vfb2|W)G{)ZdPPRfhf*m{7vJ_v>bi@i!JaOwApagaUlh=n zgaqnh+ev9*^;QrSfC-QrioG!n`$61Z5^B?QH^pVK4G!? ze69r9vcl3}Z0z9TBpWnrNjeOc&arA@{UwU6_#oPO9kX|*G2=skH+2J2v?Qu_9e(gx zy9BlwOjTs`bS?L0*Y~E;UH$r^MPNqCkr7akBTVJ!li!}@M$pur90UUK683Z{-_X@}Q_bdR}fGJ^xs2yyYt98oW znUz#wPdRQ2P}1WZMxAN7D91?zKU<0s_OlARxVuR8C^vQ%c=!Z&{m*T6U5uYGm*BK- zXv)W|SyfV}gN5`zxSDN#>Yal?WP<4wkk}~F+qQ$G+3u>Bx)cRa*P1^}Yqm-UNbQE%9TS#_8b06nqXxFBC)WD4a?Q-LvQNB|b1e?tq(lq)h41%OG*P2y}+)3ETt;*fU2 zBk}!Ybyz0<_w-ZbiHB`m%Rn}4zWaWaaW=fQyK|i#NKLlD^Z<`^(qy*{8x64*xbJE2 z#6;POO6x1*V85>#tS+Y?exyqfhqklSq$HpQX=y@zy|5`5i5xuJgz6a9@PqdZyaNXI zHdAATenQ4J24@l*19P9|{s1L`b;3p|qIR(#-52*o-2Ht>$xz~S;fVJyWA~HIV~q6k zhak1pgQExnI`e6u_s991nd6QCm`}Z(u3h_P=gdyT=Bm4brFW>c9LHYNdJAq zcbe(m%WF^?vInIVJUmD!l=Ch<6tMk^mv&7;5#b4P|N2cD-XJ3^ky>2H^n zG;lv7$YWJ(miRnMyr;P9JjrSXO&X+O5L@t_oUVQ(?GWmsMNZWKrExRqsR7kZcc~AJ zoP*F7CDaaWUtA6kV7?Ao-#|6gT!H1=h3vr8GW`uBR~^2mZQ5okTwIB{S{m+V0dgs+ z?jgK7`5;(P!iEk6cqtNybH6>sCsz?j#>!Wr2rSwskXZMvnWXM_7RenbfbQ zQZ?OQog?-Eq9C8LF_;?_l)k#wl|k&rx<$~j0WIdIuRN0-Aq}d zIvc?mM%jBN6q+rsDXF&m|V=@?KTVF%;Bv3cZ8UA5V! z%}wE8!|0DLC6a{$K@;NPG$Kve=NWfxNP}J`Tg!Bf%K*J)V(V$MZHGEElmAWun%Hl5 zo7>cB(&|M;#Uu4spV%Dn=wMdTO)Wiv1hSv(f8+^~b5B#gar#6WEk^BKdCUwVv(6`>L z?K98L!00C-CAf^(u0wo9RP%vE;m^$CnqE|b@M`TZshquo-xV?Hsgx{sOBwLb$`kA$ zD%oLRTZgRypmUV;lA(nm2b|oWzio;A;e9IkCZoigI@p|9R?uv4R?b@e5$|gBEM{YL!XiV>Nz+Qmq)N6f=462xWwEziTAiWOwZogeLM5aZy$R*xF z6!nbmZ-(lE#PR?j8~{1Y2HdRm;k(IfB@aUxv`e9uO`L98&i7R5c4(b;hShg+KVnd- z8$Y4Cf%*n1aUgTEe=L&e#OeY3=rK_a-6SJ=CU8YmQD2^LC`!(C1Lbv2Fo0p!Sw*!D%XoicF$L_*gIX zF&n-5r%5@kbCZhh)KikXbCg#+Fh!+VOCf)F2bZ@ZiPSPHMby#|35*dPxXkV+RF|Q? z7G}Xct51j{HH2Dapidw-kg@f1sL`dh9OJL;YA%dR?vC}e*-QbM$z9r#CQR|;_G&5@ zrL;a(=i#~d!N;f4JxN+j2`%91_lWwupC&D6t2bL;6ZZo11u(5it>3Pnr* zSoDJuWo#ccOvHk?8nopFi*jpKPV>G;IcDhyH+c>Kr+QyWf1M{bR_7fpo_BiPzzw7E z!DVO5I;TM17`~kGt27XKZ!^|X3-Dab;CF@TMn(tNW9+2QKoJFqeX%aRtraC$gW^J) zlaQokHmf%5h~~xKcLp)iCwWJn^GjxB^mS)qZ0xecJUK!BnGlU(WItg_4{8{WUG8e) zZgn!995(n_hzlqys94nZd^UB>9qPl)0RvNo0fz2CVPc^Enrx$u;ZyY~Tt>-Fb-mu0fu=TjB9#SCG2gHJQdFQdMxRHt>~G^GRRzB!@}tl=%1`Q83Xx<>c*;$37J zL9R<}VBk?EQ;4oF8Q^`Q)b~6|v7KQ#!5Pu@kE~!6xvnT{%1e4nH;+zP++|G1Kkr7v zU=1M+t<4d-0ZOiGtD$k(X0D&j(SZU#1c5<(dHo-GDsEj^II&WuwlBbH=5IfgS&>(d z#-(hO*OXmaN(o(dq8kx{2$8#MYL!{=d`PF2BRUN=_hY5INY;mK5_UG2J#~5hlWi(as=qmx<1-|z86(14&-TTc zl-{d3y_{}-N0lRSr3`Lo>5u48M*k(L0zHsDp!r{Nkz9yM_~RQj8rPuLGsK33!=Oe( z+nW`s);!KaK)D^buMrRrFAD48Yx+>*c=rd8DYh@Z$R4l}Uo2fTuX0vK|JgRrXNsu) z9@&neK8@6pLFFCAH#a227&-Op->5YbW*J(?7i)Ca2eRxeC=2jZP(5#9xrYnYGcrK-30&yt3rZUp8#NpJ^R)$|}Y3Z2W<)k@nWIXM75-hwi+C=1{VCsxB@(_nygHKY^WcOTa$5 zlM~|Czw`6-pYDd6&CKj}j4v?O0Uq4x_P+V$&qiH>-m%1N$u&Xz$II9=c7tpj(6wDV zEvt?m7;a^_(R}a@^#OJF0OXI*vTX@!B|)90%Z|!iVv&Wjvx9k3+|!_i1!3EwA`l&}n8N2>$pzWonLCF$W(Vbz3DEOHS@um}$8$7GZZVkshZ0{y$f#Z;nU?R9l=wQBbe9-M#tV-E zy1f2%%4Ib%p3A86gLI7Id|Dbcjos)Lh0HbBu|Y2Q>o;CNjA0sPRLZh?U@An(veY)8 zew{APg$80M>GDmly>LEce?CnqD69g<5*$AsAk@qA>@fS50Ap&ce&}@8vij(OPW%mWLLh6XI&u%wr0fr2 z%-{y|Xs}!MoMO+qm3E_xD?fb{?$Y`b4bLO>WL24eUVgoweNjAxpVxSx+p+Z>H1Ios z27Ur#;MdNqXB0oE;qj@3d=Q#++5Di!-7&dr_wG+DP^8)aD8>)@vXrvgVrAH}qxV1Q zcD$HcB+#0Bz=Fl`2R0MG4`rITp#_dbp!PCfX;z-eXzXb=e{;9-z`z&9s2jm)AZ*4; zELr50Z9F7$=KJKJq{c^2Q$DYV1T})^DCbVo zb#%<_l1_*>eu4h;#od7^P*S=yhQD9DAnD}`CE`09Hvz{e^xtj*LY?8u!Dx0ujcM3M zzX`>Sb}Lp)N92h%CT|UY4b>I&fXxmq`n3IjrFSvGVusPU%q zd4KKe_~#c#r;brRsCtR*4%_mc>!#k4eA3_fJ&>{#R9<5ztR!dy0yqi@OG zHu{Aa`LDunFCGTARZe$#2l8)O!+g@r3fO5nTF(~5ghEHU;|Q6fH1e~N6X}Lp*f*xn z{Da($^cL&Gx1!@uO7R;C7so7T&v=efI@gEno0Gf#OSJa<%bxGkL8*1@0d`OP#yk;P z0d6|0LH_MQkM12YRu47{0C8q~BEPFRiSrxr?o!cIv>^SfMenJRh2RPXDCz!;olAM2 zT&q0YX@BG))eOD=?3XW!)wKyvP;&_me{72?zolZo_3qgML44wpo>9f(Uz%RNO1bn! z;nJ924b)3oiP`0Q#(MH1L(N+Lf2dMd;qF{rlwwx*SpMBX@9qy{tPVx|DT#R|{=kc< zax@b-1crBmO~63Y0Hl4-q;AO&%Qd>Je5lI2TsvIG{)Oq~&t4Q!{sm2&u8&s@L)CK= zzBuy&HV-v0s_GDuGHOiA(=$=(Vb9E!TV61d!$k8i83LK+B)zEAt5~x8*B9mOf2FdZ z2|i#4COEw}H#_08@(*9ojiYPvhuX_`A1<)}pC%0Bb|xPzNz0H zT4HYU$r9@?=WSTOD)5JCzZ^5Y{L8{gpL}`loyn$V8yD6aYVx9fy_#q6@|2Tf(%n@} z^k88_$$E_BFmE-98{H5q3c(rE4lj(&In^{M$;lv1h2EI_4lp+7!~T?2d0}i;hRlI# zOHMt_Lu&noJ6>dBtj&389l%GZBF7teJ^z$Ac7qpl9kr*C?eRWvAK!itvJOF3qioLb zQ&!$zFfpEpfpJmfXNGID{{fk9Kn)9H#-9TIp&)x+&?(b$g10+@OS<9-P5xsRAm;8n z{wbLDZO zus3<@jgNsV`2hIKAwiCsg!n!6wva0?!_&ky<=t1$Xfrrx$d)Wgo7=mvJhvvV<(*ew zzF<=PG)7aYprzSapIq*jCcG0@1WVh`m^w*3;&|lj+U%Xt=U=$5zfrm{=*VG4_@yJJ zt@U@_vG7ms0voqdUwEItQ9F>w5hM)=x{4f~N;IKG(qxVVRyURYh-i7C_hV#2${2KF z4LI(4U+$oaTlV?~e(@<(+*Cu8HwTZQVKW0(lxcki4W9&Keywm$TF)d?yChqG?# zYZljp2X@N3YLI1t|F&_vzrb@YLZthimR z2eD@UuUAN4@T!LC+7rNr|FrS%ZTNrT$~^bNBd*ytrq1>D6PTW4YfvUYhfIZ5y|CFR^{Hz0P9{WX8H)lh!`2b8)~JCUUoV zJdt!o(A~i|{5%$6hrbYZ3_bUhFMNJX_XI*~)6$uEdcV9B9dL|H?D2Yuqv2ITF27W( z9s}A(*UEolWvz#b`C;Mry$71b>0=;j%pD-W&B>9_=(IEtki>bB$7CPs--BGgA4Buw z=;D75x>yIH%uQq04?tV1;jxgL|NTUp`CE`b_Vo+&ztki`6XF@+`)DqaVYP{6w4JYK zRQ~(LGQLdz_bk(~Y1hJd=n!cPPIEkJGHxq@D#o)X#yW446Jyh_e`#?A!Di)6FKBB& zp^T5<(m;YP&MPi_;utVd$U-#VUbFZT*R7#`lanns7#TxER{tS%A==8UncD+0v^QUo zbKf;7o^eAZCII64yH})!0dXx6c3kUp2@MN7tZb?!F~S|i&&h)+yFsRS)8A(q>K&j9 zVTbujq{uNyWl;9N-I@Z*3l{%<3f^2YC0>%7TXQOWpSvI7Idcr*;eC)c1}Wdz3A+o5 ziX558%C*D+?CSx4&L|1WB2L44IqeZs!5bL zjU}`!oB=Uwf`oDFCl{}C;Ow&*gR!i4fm-%Era63DUiL$I4MD?#zj{3y@C6y*qK9E< zZp*hX1oNTXF||g#=QU{wFs4*DEVHmF?0&GYJAYnpDdJAYPAe^3B`CllW9#a|>!2z@ zMbce)eWqd7EGWD-acueTA;tTk%jGhk``%!__XPUZF-$EetKJx-4BxkpA!u%PR-BB7 zks{VZFgjJR_aCqCAOmcxGZ|0)f`YzzdjZS97T)0(h?FV z>>*zn1$^bip;Bb$v#@9=%WOJjz9%9n!@X`vT$6^(;)zl{)eUI$WPtMI7lcDM>I8m5AMFS|T2=e>Iyle(~RKD>ie&^b9 zlh#gQ=?VzYYXx}BpIg6ZR_@Cp&;;kKS%3NJEwu7v)N{TyR<#T_3*}EWhG3&cu&@<6 zlvE}Ra>DGD8?Mw&9l6x24G@%!Q+YC^D{G=Gp(|(Po87Z5z6Nv@uD(x92#Mk-bpzhf zUL)V;Pc>f*xz?jBRh3+5^Cxjlm>8cXEY;V-+ei2DqfJB16JOm!|MO$O7wbU%5rJ92 z;#&_?gX)luI|MG^jtsOv^|Tzk^T;rZ=KQkS-Dje)=x3x?yJs6Ch*1JY%e_Z8vH`8^ zc~t}TDOHf0}1BGvyHAS`=agq3Awt*Jsipg_!tbTG4)Sm&9V6;A|+$z=4 zlhWV~YZhB9XUupG?)v?p`;VUu)qJUm()mI&5xg91DDeF$@_Y*t1*`|!?7KBZhOhK# zyiy!q7Db+dw*1F;9}Jl_sl(6IzDMfAyme2h#$*X>#Ry>5h> z#egw<$p3NPnOB#vp{2q_CI_&qAq>py2-uuO?8JnRZ)<3QW-8%Q`0R2Th z?)IARSD|l$`1_0mTI&x+e)-oo_c;UI*1n3?AF4;)vS#s}C9kfx8wwhJ9VPF-Dc>6k zNB;dpIrG((hU0=9t4{uV2~s*2ss)iH=V4fQc#NO1_VQU5qOJikG+884xOlxP;(57N z>V^s<+wnQZaJWm}jC%$3J<4WSkcZNm0ygH%F(u@gFx0j2gq3oklL$^1R83XFTz zu82|ITstwoa;7@>hLl1aqbOEFrL7IR+HCa%Ty4@|2JH0;w)rI%KVWB8ehJfttLMYw zJh{)&v6(yRd}vY6nsUY|**$6fnvM&0;>fVMW0qR2V@U~;)}Zr#)uxD1D`^+lKcUx( z1IH%bH5O>5?1Mpu%Z}$!{%l+$(KA2(vc3HOCZHM@vk_k=#{PBcRR0aCp~r{tr@a&ZlZx8S>jdJyTG<0FEGfI# z!Z=9jzqUGs@_QapC=JBQOx~)SPtjMkd&cMKq+CJ(E0|B#P4NsgoGbyase{`Wfz1tG zn@4snSY+t*`p)VLzezdTLVEO4YUJ@q@k70QTEOq@8>hnS-CDhETHLg&lB5~^&$6PE zv^C-`86Gi@XUF7p&;G{Muk54VIrgr9%1bvcH;+=Qtf;PNTqFF*0Xe9)N+Nd;@G`}5-A~zALMt7ItTx;|?%<1dAoiqLODz)e=?2A}82MEVqF zG*(mPoq}}KV)+uU%p_NLxRbX6SrVXrmgRn_g_%kVP?iX73=iiE8+h(S%?wo+!a`an zI&|e7X{Ujjl2i$NV_K^CL9~tGu1AvXk_mJ4{=eqA2-{`CX;}R)GAyDp6EeAQPdoQ& znYRs=+u?~72%iNp?YTH^nVN*;>(3k(+6HQ}BYA<^BYGAW(aX4quDTj`rNN2QSJRU< zePGqsDmNnG8O}9GGb2EK+##-j-T1EOGBwzplfS^od4b;^bPR81xG`DF$f-S&{N0>7 ze&qy8Un+&<*t8V?2g{VBWx_Tz3P~TsWODDlnf#zQfx#nkejW$cuBim&PZWhJtS4!@@Amcz7v(D(`euz?#Z*%YmW(pl@OJR?+d5xAXxfvdgv^oAwjh^8#r zo71(X+A~JDwAQt5JLLU`Wqro*ri=L-C@D*wP5Z&nvY}qK7!Fro^@Kgt1~uiPE|sEb z=t1J17qZz6X(D}na@m|>%I9sr74~VJ&Jvfakz}mEpt(j=VG^`dYnYdrMAFX^AT9jZ z4o1dyGOqoqCz9TtHOI5K>{h1zc9|J?@daYctpc-K-Y2E*@;)BAkZbsY#t2My&ntPF zE|2*_>&@@+x6$@`RFRUYCCyjc<-C(o*P-5S2Dd=(DWL;d{Fc-kledpl^=J#Z!^`92 zIaIhsQc@>Z+j_XZ+x1JTp-+amb`=Ob4FZ;Ssd=EPtHC`~9vB|0^mD)z`oJ15-8^sP zamn(ppAM>mccrVc2U#BmfEwC_gK!8**CfVV9iU*J&-Cb)VuJ;kPvlRjYojzcgPzZ= zBC8DrDheegk9{GA7by<+E>Wy0!I700z%Zp0gM?C&fO)b_c-zxiKf4S@T%PZ{k(&0HH_@_&uh2oPIP{JVJ-0f?^@lqeT zfvrBPTRYDJoSAwdYOC538T1r*)`NMnX$*jOHAwR!`P-S4FiAFN-)wjN&q?;kgaEZ# zPQbCU6epZG2Q*i2Ywrge7`nb{Jg$;yj|K?;}5vCQ2y9 z`p0`!ed0BAxUOBaQTx4|>#l2RUphyciz54GcbSh~;fh*su@~ou|oNil6kF2Q( z+@Q}1Mgm#%Yc9{El%O5D7%4TTI7&q~7s0p6%la`oFs&^;!PO*SNxL2X(pO&(=}c(A zQ|xkG+xp~#R2S_my+vmQJDryFu6<-Y?4)PxL3Lv#9HTDB5}yy8 zd8YNA=tT{Q2F5U27hM9&n~>FR&u>@?3{g9Xcqw+B0MTf(YTY`RK?2XNb(#+_+hhTm z(UT#{vzw_cWVa{3sYGgcBa0rH=qDzFiCLid2iG9KC*x!63Yz^ijg^=J{f-8bwvM_? zy%LoO=(U!0xHuYS9sZE3Bl3)rNu~Dv5GkJLu6USaM@h}owfF`n6PLr>GeW6{L*!R1 zrb5G`>;cV}XmZ27Wi)+Hn&=wZ?o%tVYsVF92#W6;-mO>OHw!wAn`*M#X1y!w;={r@ zT-^;za=yO*XDPa_b?WD8Z}D~(Fi;FwNC)7;GVyjcA7he5`TW5QT%1zD9x$qVyVGGX z{0E3(HM4k&`mz(ZP-jqcxq5y@16!oR6FH^>5^`Ir#kAWoc?KQuZ4DP5wC=SwEf=)N zYOBSBf(%WL%LaYc?Cc(cHW3T%Yw} zMBIeN_BRB&u4(6f%px6VZy(rE4hKX5errGsUv#N0|?;k4P100j1Ib;D-ZsR3&pp6Resz3~CFRzof$1w^@=)iQ;Fg({^r?2~EbD4# z<$cc>3|X^}*xu7v|2EW(Z7Yrv_SNET)jPFW%hRK3*HwE0D;O&}C=bVTq;4rxpB#f6 zSBP_f>#hNjP1(hFzwS%G2s zRBEGgli-*<_Z8MNGeysW!wdD#_Le=r?Oof zu5S|~@OB{vsqPr4JHb)q;_N8xdOr3So6VsQxmDqkuEjqf+U{VsC24T6U_-eFW}r`& zCU$k5;Bq}faXmOi%o4~RJmF-Ck^H5b(n$kOMc65Wz-#W-eHO)Tn9J$CEqk^-FCPvt z!d67OS0zy%wT#Qx^?JDP%BVo0?P7x~E#JbVl_=72+U2D_@EJ`uiKt;T7;Y=hB&x2Z zA`vVc6JPo?pxSeV7vlWjpcu@?!7O_)b}Kq%BP%IlvT9s83V^Crv;L?+cpoxRHDo zo2WNpMS2&L>SV`Kb(-FXFb;%)eOOIQx%ztSiO>05Rg%#0voSaWqXE^q+x!znJ*@?K z?79fz1LAFswf2H#RYnUQs*S`-T?5(%0EALs6Sz^iCYsCT7ijs)$&^6+IG8073LcGY z#@mS(S}!h9GNk6Ut^w*MmX2^Lt;steKP;tze+`RzzRlMvP)$^1rn8R0zQ>*bkCHHBLxe)xOa-fnGnl9%&qgn2YA zuU3NA54Q^#Mr|(FQh#Z2D#}z7l*&A+Zgfa5JP1!cH2wh=^Y!?e;Bv>Q8}L|Z)aZnc z$%Zwx=>qD;uJnlLG=rMaIQ=Q~E`WtSl%FfwA4v$Sp|qjzjEF+WbnV-9CmtxOx>XC9 zddqDV{_4j9hEsg1iO%CHBGNV}C6#vHjbLD3uKt2!r`}0jI5~D{79!om7TY{=N*0(Y zX)K9`B61p&I*Z`qKJ3Pb)A_f|mW;0sm+yMDEN}YiQ}_7Z5dZ=0lUx(m6Xl?u47)Cb zbbh|uPM}F^bl01#iB0(wJ^x4ZZC?CM*AUgj;)j>(B~t!jiJb|iX7nM~JmCEehG;bDU-n8amtve*fYldDXNi0ebDGUEZ8TK`!J z!Nz)ZCE2xUDa05yllBr$2FO*Lc=`;izdj?a6V834STuolW?PqfdE%1PQzMspTfeF?Dcu;oz=Zh*RKTbdjRDw<7K zR^Fs6nqU4N)+dj8z~$Qe!H4T>QKXW|TEF~y-z#|Bu+p!H!ApMPym z2b`OylxavB1ro?)U|Corfjl-$p!H#Q1xyc-HZp_C)nydpZ`JeO0U&y<0}eusu99iT zm#^Vj>ECv8DgAvw9@(3*!)!o4i~T?@p8SybMj*&{@OSBJIy7(6aw>Iu-w}?Z;d7{d z!d6MEJXe}ri1-N}gz8S61g;lIAA9Sp;)?L5Z>ekGvkm&iGFY#^UPhGT0R@F?I^g75 zdAPJv$L>bYI4v9vbVz>Vs`Xa!T&4z6?UPlDJ`x&Zz}s|xXkze)PX>^EGL6F#Rnc&X zerB>#TG7t#*O=D26V;npyao;{K;I-#u6ewja`3@cA0foPY5xe*6itSt)zCFL9Kz4E&@irN-)>{gleQw-A2;WXd^9= z{t}0`H#zzdLm>p1-BlEz3la3^tALm}3O)$-3LvQX+Mnsy%z76sneV3`O(-J%lPlp1 zFC)r;<=@2Azx5>g5|m%rIi=~*7~BSs^hRoYd@sIBN2Dmkq-ADMHDl)VNzRkjAa`(| z5h|1CKim?C zitiw$$>x3Bb((nbxXt@eUz(V3_YkbC?WK|sC4iJazzia53?=Eq`k*#JV?4=XP=p?u z6d6-d@h?4^=}louDX@LywgLuyZ1K1n06Bx6GtorxBSe?0;Q%q0wTvw5K-zngndn#Z zU*qkxGK(9a5fT%<-=4O2qe~WLPU8u(br#`F#c5$uqZQkZ&C)(!0@M(BHZ^9xbw(^G zuzJ3x6_^Zl)qrIod=U`cE}m2s13FT9er2SVYl{Mu&9-i$g@vyKdgS9W08#}jzc#fS zl8dP|yj&K&sfhS^tW|E1?~y19OpHXjzDBhw(^4rG*qQ?0Sr4gxp{f>pT}`T3M{{I z4gy^cxBIYlHnDG94)`J!ou~k6?{K zdiWyJLv{ZPNYb=S;;6r{nZ)mpwsTPXRLb7#V@CF`B*h$QWqdK*d`2k7Mn7*d1=`2X}G5w9{QY?%I=0nyk zUJ`HzlHQ~R`u?|3Y=VFSl2-PWd+R(BOJfC5@OCWo_!c?uSW#fSzEiu2!BsAIa z!&NP5oy-N&CW7`NFz5(EA1192KMkTS6qpl;9HPN0GVG(5_$xoUH^^2*Exhh53~ykuvz@Qer5!tH&Dg3r5{thjL^h&>uNj={28D&c)BEjq)$RibNNxGIWQF3 zRKPF6a+pu|v)D4y0Dk9$)|4@wO6G}_?}y8BXY6J=s$LSUhJU`rAC2cBf?QBqs+qQE z_0tzN-i(G4fFpNQ^9qZYtxvSM?7&V&0BqKY%^Vbj)E!mPu-sjNu<}taCb5xRm!x_* zg)vvkXRy!NBt2|UDBS0cky~TCd=9g6g%p00cG+SsZVQI?BF5pOwl!cTsTJiF4&y-JW(m4!0+M&AulGMTR6n2?5w-e zf!dA^Jf`$mr|E<@Bj#3}nOY3KQUO0h=_lhK)Z$tIPe~O!+|=Wkh{1vG31?n>@_~#S zj;<^Kj0^FcK`**#5_x3u?o&}G@W4Le5)SsFbyLh!fr`j*JV9lZO3Bx)K}wa{0z5gA zNw}>Un2U~mZZ*XiKZlB^*0z|D;PCSMt09GuTWj-Px+{$bm#&#Ca{)!gl=jw=>yCNk zW$q*+O4Upv-fu!DB@&TV?|k)A!r7>BUstR!IHU(>A(<~Sha`TwQxaI^IDN+$3p{VQ zPA5Gk%nHI%5CkxtzQ^&>h2ei?F|;hb7D`jVg9G;+}r<`{oM*m~WvnZ1XWn?DH_w)M5gq`QL_72`O>LP-dLG+z;-k*IQl!kyJt~(BA1J6XB%yCj$#Il>w6=tI!Uo z25q1L=YuL&JI`YYHjqd$=4F`4@ zar#v+<>wQosI_yoqi1NVcyCR3Fyr<5X0}-xM*FgeM0{4Nd20cOYdkYSe#f<0VkF2; z+tsgDHONDH$f0pv4?$^kH#h@9{_v<$FCTSJYHp_r7070;#0^`6!%iL}+|~90`4Q__ z834om-jeMmIzH18Joa(19GwXgNQVxAmG9nBYk3NG{b)?nkfl5B-K2YWI5R2fefum> z3s?N^@m7wUsnS1;tGwl_o!cY=7$^H@(EDm~S0}TV+KavGr@x4<(p|mpB$8=#duuXv zZ{~*SJVYmKtEP=JIH6dkGI|J=w=^-`gClx*13#`WKw?=4K8XqgC zErvS~nxGm(eNc7dN52gIA#~D5cpjhDyq*sSsg7GZ=$~hWURbX=hlgZlaGjDd$y9mN zs&9rh@ssbhWXv{(8n=f-D? z^vOB5YbH;I6;@YQPqrLXt8HkhpjGR}PI`>6wM2)7bGFB{C9OKORN+~&O()I91eDM* z2U5+VeloNi_a80Ca(K$CQRffY#i2$Y_PSH*9wp~rjiAewLByNKTYoY63-9*U#hu5K9&_6y-jp2<4 zzd%RMpZMdRlmD7CqkiF=KkiU@d%?_RZ6&%ZnH z|2+d~@7tc0RIur5`^e6Pf$-B%?TlsZs8ae@`*AA4Bx*UQ$()!YsS!qo zQs_z}a#dN$KYo(ST3_7|aPMsy9B6pPU>}_Qr8|1KSoX{abvsKFS$51tm%N4kgI z7m(X~oQ!oezZ-4tHgIS+&~p$}`OT*g{H;QLdsk>hC+7bAi#~fcm;M_%61GDu+uUC4 zQ^8`ou}4u<{tYkRxRUW2BUU5H&sUPyMhv`pjZxMS9mLm;m2()OY8i)BxY2vL?oGvx zRd)K@(asL`PhR*sH6SQ_9Sgk`Y^`&R<0u z@g^rv{+J~1>K;!FC>P+hQ?%q` z`lj?-xs{~-a!MAksr^Kkr%x@vzH+KG-{gm3%Tvw{>h9s7z@KvCQqO5BPHg!M0(aPM ztK#skzZjzuk+t+sqV^@xWE!;g;aDEV*m3%_^>kMav!|6ga5uV1Oeo<@P5Gx5PgCvGAT?Zs`|_*!UN3 z!K!KNcDyZ%JV$RiIQ!5lcL7bJzV(bDDYlEv@3ka~t%ivg0*wW=e|%^xNGs)^84Nk6Ffx(YMHlBXxKchr*#NdFG0( ztg~l~t+0-sKzhl%8(!Qyms;n#zIx4EBZSNLv)xrF08)ZNS4 zr)Pb0zlv$UaZGw)sIux#!}-EZ8ZnuHe>HzZwz+k$MPBkr@y%W_wtKsE)vJ`#{-vIr z>F8eEt0%pAjndfngDzeDUyq7UX(9A!6_ZKc6+c2NpGpp`S9VxsznbtW$CuY zn&{>H>`b)&N#FW{RS|XD$3$+!p~=qSPhOe?1#Vt0)xNYKU$wg*D@TirgK-*?Gg*Z{56Rj}-!m@h`d%^2(tRGlDW@mEzZHk6WiNAg>C*cb5_!Nxzh zJIZ5wdde>f{bR&(GI#Aj7-c_-Um3tXMGfvIi3Xf9zQsP^1i7sKQsI2@U&n{}$C|IY zG%(H$Ua%lC4QfpwUCdC z^h(lRuxJP8y?#g%=3<-4twlp$JlS;yQ{o^h!~}TiD+3yn*ChV5H)(9b;@S4TIHiMs zf5?H}!7$pjNH5CJp~eEuqJ_8^r0z$P4&4gtXwPkpv2j~=3O-d~UHv1PF?O*RdoLj( zsvv3tYkeHwhIK2-UDG{XoVTGgC8BuPZQJ&-56N)mPst$fga-B&m1YMJb?J{Q0-Og< z!m}*yA0wIf-J#CAy!=k>IvK~C$Pg9h%j>y1nKO8EBlcRcirERJmL#_(&HM^Z9kUwuurrgr5{ul z24f|pWr?xZ{N=TiA!zv3&d!uwR)shzjp=uQ0-)JkQO%7iyfVn9ZJt<_N^&pdPzbtP zWt0P8VDyVSec_*-p6S0AW=&okKr(}()3llUo0NuP6*ZRu6+2?uv$s`h`!d&H76 zox3-Uwp0NCX*L`6oUOdekbHqb*>+)E#tzwqmRoq{%zM(8vb;gab}zqQezEAi<#iu-e4xt&1Fs8zTRL8-cTsh7)qV2*@|+_Y-*HB z)!Xz9SA4rzqpl(_q237 zeX4lfwi?wn?sWfPI^EvFT&jrkn%%OpXxsLGjHc~#6!k@W(l6fFjrZIJaD_#^jw#I9 zv3)H3nz?^`VvPqKLHKS{AAE_t+0(4(2_r!i?s7J)cR8PLC#fp}A^6aPJRc`4oC>`V zR!S!zDg}aM_&)(Jhch#!!IoRAtqefQ8Yz!`Fejel?YzQfH0;o9YJ6`ZRi<+V^pK&U zj4)CAi{>m5w&osoEYPU?8`1N~J*8kl0N`7X$JON$GjS5t;cn9#GVyS;r+g(9K3f(U z#0}w3_#gMq8DAm)&Y*qzN&++)4r|BK76yc-M|(D@N3t>Jbh#xJk}@0SREAypLLAM* z&!6X=rk?GB^!dn}KPZ(U?@XXRxc-8mu%*bA80$mAuT1X`Ih8bc&Xgs|!$i@;WqtAK zGwMg$1C4R(!b&Q3)jSGtnBN+gCx4o{dWph%pYwb18Dqz9`L_caM-sphm&qXFW>9Dq zZ@>5rWo`Gsud}?md$cHscyG!wB_ub7(9Ov;WVH`!#!jR|e{g#PdqE~KE5nhYtxSvq zU@Lm;PLl>CMD=uP)QwfPXW0>)70^b#nSA$-xez*xyeBurUke}LKYJ|b;uc`}I7%Nq zK*@tC9Ta$=jM)=i3fX+Y(S7GNLHnX`_a6F{cJ{U+KOYKOKT=&ru`7i>L;BD=|IdhM z1m$f~jZL<{^Ta-_89G$g+S%3RWk(m+8QJV}Ne(+GUclukyDh|Zq6h9~+XgQ`rUlGQ zF+<2gC<2+S@|%@Ysy z1TrrVuc!fh=}`_ifCoY2s&#)2{{%ptEzuKRZ~%R2yzmR?d{OwGt3{*#KL}+4Z7hA4>p&|R`ysTexF>XkFpDM=)M_tUvCKxR z^yhD#$6p!p9eQz64h_^V_xdr@QIP;jOMjB6r1H(Ub%XxVK13vjA4JGgkURDwG}u=Y z!|If;+>+2)W_vKlugBYA`^(2M3FA+exh?)kGyhVvK_#3XCP!G=jmCA{1EXG@JInXh zsanr&P7k8D5ON7Jv}85p7q5Cc5@>Ai4ZvgFDJn>hb2`t_)g69VU3vpUqUEFYKBdK& zsL9|CPLJs|-_m$>P;fe+N8PJNw7kA#R`^Fr_`g9fqdSW9I6ok=ZsNFaaLJvZ$jCUx z%!D>-CV+%NARL z(0ekJ@bK7YXX)t|>rVzR|M@qAK-!a+JUSA6Y%hhW-}6xkFaDFx5~aVht>XXUT9vv}sLkKqLS5bYe$fcM^BW2wizR znIZ1?{EDjUp|A<)*f2Y99G;@!EG}pYO}T^SClkt zorUo(Fd3uCTuAuZYMR3}zglAIk$?k@^CM}O`tn_+)m)-A4^Ylyn7!@}4pmwV-FFj| z&{D-Ax#wZPKSUH(sgxbfMI~mEHw(civ8s4Wx=z8ZV&XbDxfJPRTc)OT&=HeFdv z%k@q93bTy7o?J9P9#^xYlhF=OH6HsIgn!j6iy@QGD3yCiq7>Xgjz7_3x-Y(v9OXq? z)?Zt+?$Q&GZn)L2mO(j0>sQb~o9?sww)~s}lB;cU+gGsWb#8v9!zR4!jf=p;k#^Cl z2(&Of?!&Vz-Ixa`o9gV5T<@Ro zX1!6ex;FwhmV4h`?X5anx^;?&ZC4V<^Lw{aoSD$T&@yOa4)Lk8z|FF%s;XednIt9o zhTOJqi*6D_ePugu8dyU2gEQKsqE`yX-@PW)zw3G2Tm7wau$3n2!CJ$6|sX9 ziNnvU&=&nLaMz^KemWkJh?^3w=8?wKKIoCXTckgkU8G6x({KaRh z!o7_0m+vYb`l&wBEHQRv`LGRrLCl2eKu)nOF8=VBC9WgW%b3d))(?Qfzjq~n565ke z{qU=plYA!PMl3S>&VDTLbKuvY5J@Ijbze4BL|pPQmRvr?(&QIER}FRSn5^=hHck=^ zx!PFg$64x>1)(EGuGXds>kXQ8yRu}OdW84~r>b1@DaRr2x?*6S^?J67w;G%?XTj6C z<~IC|1uV*we~uaA);H^uc7iq%+_sHe{pV3q+Y}ovjL(hjz*NzjE?Mz66kz)k3r)N( zsgX!e>H42f*b1eQ*NrPX+C5kg{PzOR;W$>*h7=HYd(|OeBAdnZUHE7M)_%%rqy^B% z-zM+hGw}Be{NFwU&)?brgJw>|kGpprg8tt4`FjTbo`L@dX5iV(IgoOt!;XdZRgd4~ zD;NLAgYP`OZ$ks|-1K=)r%O(ynqe3G^=~B3s%!Cl{GkVgGF*Y*{g0gg*lZ5^ zd1lHvz?iJkzpRK{@oU)DQuz0s;v)wR4?qK#vpGKmzqs@xOC`SX!Y{{zkY^f@T) ks}ClS;SKMxjahp0Rr}@PxP9p-khgwxcH3S0{gGe(55K5vZU6uP literal 0 HcmV?d00001 diff --git a/docs/pictures/teams_ui.png b/docs/pictures/teams_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..e9daf8e7a92c7de1beba03c7d12f71e618f86951 GIT binary patch literal 14980 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 OCoHXxzB%l2^M3)8_-tnY literal 0 HcmV?d00001 diff --git a/readme.md b/readme.md index 07bc7be5..31001901 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,13 @@ - [Setting up the Machine / Modifying other Values](#setting-up-the-machine--modifying-other-values) - [Troubleshooting](#troubleshooting) - [Problems while Running the Program](#problems-while-running-the-program) -- [Microservices](#microservices) + - [Problems Installing Software on Raspberry Pi](#problems-installing-software-on-raspberry-pi) + - [PyQt can't be Installed](#pyqt-cant-be-installed) + - [Numpy Import Error at Matplotlib Import](#numpy-import-error-at-matplotlib-import) + - [How to get the GUI Running on Startup](#how-to-get-the-gui-running-on-startup) +- [Advanced Topics](#advanced-topics) + - [Microservices](#microservices) + - [Dashboard with Teams](#dashboard-with-teams) - [Usage of Services](#usage-of-services) - [Development](#development) - [Program Schema](#program-schema) @@ -145,9 +151,12 @@ These values are stored under the `config/config_manager.py` file. Depending on - `LOGGERNAME_DEBUG` name for the error logger - `USE_MICROSERVICE` boolean flag to post to microservice set up by docker (optional) - `MICROSERVICE_BASE_URL` base url for microservice (if default docker it is at http://127.0.0.1:5000) +- `USE_TEAMS` boolean flag to use teams feature (version >= 1.2) (optional) +- `TEAM_BUTTON_NAMES` List of format ["Team1", "Team2"] (optional) +- `TEAM_API_URL` Endpoint of teams API, default used port by API is 8080 - `DEVENVIRONMENT` boolean flag to enable some development features -In addition, there is a `Shared` config class, with dynamic values. The only thing you may want to change is `supress_error` to true, this will activate a wrapper function catching and logging errors of the wrapped function. In production this will effectivly prevent the app from crashing due to errors (bugs) and log them, but setting it to `True` is at own risk. +In addition, there is a `Shared` config class, with dynamic values. The only thing you may want to change is `supress_error` to true, this will activate a wrapper function catching and logging errors of the wrapped function. In production this will effectively prevent the app from crashing due to errors (bugs) and log them, but setting it to `True` is at own risk. Depending on your preferred use, these values can differ. Then just run `runme.py`. @@ -160,9 +169,47 @@ The program will then evaluate which recipe meets all requirements to only show ## Problems while Running the Program All cases (e.g. not enough of one ingredient, no/wrong values ...) should be handled and a info message should be displayed.\ -If in any case any unexpected behaviour occurs feel free to open an issue. Usually a part of the actions are also logged into the logfiles. +If in any case any unexpected behavior occurs feel free to open an issue. Usually a part of the actions are also logged into the logfiles. -# Microservices +## Problems Installing Software on Raspberry Pi + +The Raspberry Pi can sometimes differ from other machines in term of installation. Here are some issues that might occur. + +### PyQt can't be Installed + +You probably need to run `sudo apt install python3-pyqt5` instead of `pip install pyqt5` on the pi + +### Numpy Import Error at Matplotlib Import + +Try first running `pip3 install -U numpy` and `sudo apt install libatlas3-base`. If it is still not fixed, try uninstalling and installing numpy / matplotlib again. + +### How to get the GUI Running on Startup + +I found the easiest thing is to use RPis Autostart. Create a .desktop file with `sudo nano /etc/xdg/autostart/cocktail.desktop` and the `launcher.sh` in your `/home/pi` folder: + +``` +[Desktop Entry] +Type=Application +Name=CocktailScreen +NoDisplay=false +Exec=/usr/bin/lxterminal -e /home/pi/launcher.sh +``` + +```bash +#!/bin/bash +# launcher.sh for dashboard +sudo python3 /home/pi/Cocktailmaker_AW/dashboard/qt-app/main.py +``` + +```bash +#!/bin/bash +# launcher.sh for cocktailmaker +sudo python3 /home/pi/Cocktailmaker_AW/runme.py +``` + +# Advanced Topics + +## Microservices As an further addition there is the option to run a microservice within docker which handles some networking topics. Currently this is limited to: @@ -170,14 +217,26 @@ Currently this is limited to: - Posting the cocktailname, used volume and current time to a given webhook - Posting the export csv as email to a receiver -The seperation was made here that a service class within the cocktailmaker needs only to make a request to the microservice endpoint. Therefore all logic is seperated to the service, also there is no need for multiple worker to not block the thread when the webhook enpoint is not up (Which would result in a delay of the display without multithredding). In the future, new services can be added easily to the docker container to execute different tasks. One example would be the export no longer be saved locally, but send via an email. +The separation was made here that a service class within the cocktailmaker needs only to make a request to the microservice endpoint. Therefore all logic is separated to the service, also there is no need for multiple worker to not block the thread when the webhook endpoint is not up (Which would result in a delay of the display without multithredding). In the future, new services can be added easily to the docker container to execute different tasks. One example of the usage [can be found in my blog](https://andrewohnsland.github.io/blog/cocktail-maker-now-with-home-assistant). + +## Dashboard with Teams + +With `version 1.2`, there is a team feature implemented into the maker. If enabled within the config, the user can choose one of two teams to book the cocktail and according volume to. The names of the teams, as well the URL of the dashboard device can be specified within the config. The cocktailmaker will then send the information to the Teams API. The Dashboard will use the API to display the current status in either amount of cocktails or volume of cocktails per team. In addition, there is the option to display all time data of the leaderboard. By default, the latest 24 hours, so mostly this party, will be shown. You should use a second device for the api / the dashboard for easy display on another screen. + +Maker + +Maker + +The recommended way to to is to use a second Raspberry Pi with a touchscreen attached. Then build the docker-compose file and execute the `dashboard/qt-app/main.py`. In before you should instal the `requirements.txt` within the same folder using pip. See `Usage of Services` how to setup docker-compose in general. + +A second option is to use the `docker-compose.both.yaml` file with the compose `--file` option. This will build up the backend API, as well as a Streamlit frontend Web App. Streamlit is using pyarrow, which the Raspberry Pi 3 (Armv7 Architecture) seems not be able to build without any tweaks. On other architectures (like x86) the container could be build without any problems. If theses things confuse you, I strongly recommend using the first recommended option, since you only will loose the possibility to access the dashboard with multiple devices, like a smartphone. ## Usage of Services -Simply have `docker-compose` installed and run the command in the main folder: +Simply have `docker-compose` installed and run the command in the main folder for the cocktailmaker microservice or in the dashboard folder (on another device) for the dashboard service: ``` -docker-compose up +docker-compose up -d ``` This will handle the setup of all docker services. You will have to rename the `.env.example` file to `.env` and enter the needed secrets there for the container to work fully. From da9c4508235fc8b8fd6af6b13c7297345c896640 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 14:43:29 +0200 Subject: [PATCH 50/65] Clean start state of DB --- Cocktail_database.db | Bin 36864 -> 36864 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Cocktail_database.db b/Cocktail_database.db index d25d6dfa2b49bf6d37fba66def51c34be4d95a9c..2d7cd786f5e4ec534f6cde730696971c326ff4e3 100644 GIT binary patch delta 1805 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 fHrTpqt!N=Lm7hslcJi?JEnW|AAZYQ*e0 Date: Mon, 18 Oct 2021 14:56:01 +0200 Subject: [PATCH 51/65] Introduced default db to not overwrite existing data --- ...tail_database.db => Cocktail_database_default.db | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename Cocktail_database.db => Cocktail_database_default.db (100%) diff --git a/Cocktail_database.db b/Cocktail_database_default.db similarity index 100% rename from Cocktail_database.db rename to Cocktail_database_default.db From e469628ca6f22806dd7abd3cebc8b95d6c988fe9 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 14:57:16 +0200 Subject: [PATCH 52/65] Adjusted to ignore local db and only track default --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f39c7cf8..fc7d99b4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ venv __pycache__ .env .venv -team.db \ No newline at end of file +team.db +Cocktail_database.db \ No newline at end of file From 613cfb9544ab430f13bfbf13c6ea2be20806b06e Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 14:57:55 +0200 Subject: [PATCH 53/65] Added copying of default if there is no local db --- src/database_commander.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/database_commander.py b/src/database_commander.py index 3275b872..55475f43 100644 --- a/src/database_commander.py +++ b/src/database_commander.py @@ -1,5 +1,6 @@ import datetime import os +import shutil from pathlib import Path import sqlite3 from typing import Any @@ -361,18 +362,24 @@ class DatabaseHandler: """Handler Class for Connecting and querring Databases""" database_path = os.path.join(DIRPATH, "..", f"{DATABASE_NAME}.db") + database_path_default = os.path.join(DIRPATH, "..", f"{DATABASE_NAME}_default.db") def __init__(self): self.database_path = DatabaseHandler.database_path - # print(self.database_path) - if not Path(self.database_path).exists(): + if not Path(self.database_path_default).exists(): print("creating Database") self.create_tables() + if not Path(self.database_path).exists(): + print("Copying default database for maker usage") + self.copy_default_database() self.database = None self.cursor = None - def connect_database(self): - self.database = sqlite3.connect(self.database_path) + def connect_database(self, path: str = None): + if path: + self.database = sqlite3.connect(path) + else: + self.database = sqlite3.connect(self.database_path) self.cursor = self.database.cursor() def query_database(self, sql: str, serachtuple=()): @@ -388,8 +395,11 @@ def query_database(self, sql: str, serachtuple=()): self.database.close() return result + def copy_default_database(self): + shutil.copy(self.database_path_default, self.database_path) + def create_tables(self): - self.connect_database() + self.connect_database(self.database_path_default) # Creates each Table self.cursor.execute( """CREATE TABLE IF NOT EXISTS Rezepte( From fa07674391fa5025186b95fd8c8af7cf05bc8a44 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 18:11:34 +0200 Subject: [PATCH 54/65] Added more recipes / ingredients --- Cocktail_database_default.db | Bin 36864 -> 36864 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Cocktail_database_default.db b/Cocktail_database_default.db index 2d7cd786f5e4ec534f6cde730696971c326ff4e3..b1f4b65159e06292b6825815fabc46700c637470 100644 GIT binary patch delta 2251 zcmZvce{2-T6~||0edl>|w==dm67XTOKF45cyNR*y&JNf#B*YpKZqLCH<4IY z-xa?RKM^mBm&CK;32{OkRo@f`#18eZ9E%>&CK`A`R13_P`Az;7|1NLhFY*EYG(W)$ z{2<>|uGY4)^10favZyL+@j!9BsEx>f-S;T%c*}%YoY(Tj(zIbuE{x9@c9$pGzF^zt zxNaHQ8f60>wkFJ}qSkNP6SKONsZyG7UN4wO7PNjtAD>Pqls3HEcuXr=n)C^Rod_Wq zGv-V8bEN_Ae$+5ZS~*|;wd#@XSgn%8`xi=j$*^)vY)7?x3W?kNlDJpg6u%a)^9%fk z;>Y3#;wAoD(JU0v%^&5z6c@y4{v~;Si%*FOF~I zYe@V<{9W7@ABbDxJ#j-^E3e936&(sCIIUC?oKvC%D@ugm45=WXC+d4nJkyc!b{TwI@Ws|rV;4wHeRRk96zq0ar+BTMvNAcMM9(%mzM zwi0pMedEx5>t$79rOX^C7IdQAQsGI|K-&adfGc8~dO3cWr_??0p}XpEs!BZ{J1c%p zZ_+t&nzq69^3JOFDi`~^RMd`1o>Guz=zrh58l7oRYqJHdzp!9v0}Hl3t`}Ri%mZF* z_c7a)FO}hF113&OX!6OPVCc^szW9>{8~vF3g(P>6%_g?BYH2@k$6Gy|=BP>9DxvM2 zeHBH22n_PNHDMIyO?%;3tL8Z+TD4$hzge=2mSN57lO=bPIZEIi_bGF*hrNzi663v; zuIg6NT8!1u|KskjeykQ$l*Gha!5GuLZfaw?nLroHRYxylys7-^Oe4l;+&>mB)~FvM zwi4qsrE62FSFq~liYqHFZC+nrzC2sRUhCZFHWjsleTs@2G1=+6QaVz!rnKStf;>SC zXM&>t^W?2syMG;b9%BFcEm^N%axg6T<4KI6x7*#h*%hPz@9gPL6}4fK4U_I3MuuJv zw!Ui=)OZ!DbtA{_XYqIAKZ>7>yKz^J6vWCyCmz6y@o%7LBTn=IlATK_k$2=xeLYzw z%lG5}HOi3uyW=@!ds-;rtLcB$$zDJ$o&$xu3aBB=wLNk%FuH3fWwW~OWDUTt2yjBx9G+Q_xAo8S%K76RJ@7@A$9yFZfk{ z#r@RMV3dEvKZw2No?RL}8heEw;G^vK>>7K6U1i^4UuVy;^XwEm&Ssg;_OM~r!`fLB zTg_C4;cxIhT!**dr|^CFX55JHi|64*cm|$;lQ0Jc?2BC&U7}TJkcTb&5`Uh5m7nHc z|yj^cpx5@&f~;>KHiV9#cqVBtPOa?QW?D=U~W5$p?Hr?XHmg z`H=f0@Aoy(rECUgU&tK-oQDHVS5)$t-4IT>F+jxhsflN+n@*!uW=Saw&|7Sz)kowQ~Ofx;3VbK}(K7eK-a$$SOQOQF= zLfT6n4B0c?7IIed)jL788ejFezZ)KAo2dI-HkuUPOkSitc2{|Ilb7x<`gb%qj=hBOO8p$6e zkb61ZoW!@V)>5 delta 1319 zcmZXTT}&KR6vywqvrFgB?A!x%w-9!LWfx=#!WNd@Z41^G8rBbJu+kWb3KLku!hXPh zXcWrF10}VUYQ~WC0gExF2`@&*U}J0|))XLsM6NoIcc zf9^f!&iUV&T_Lk8u&ciU6}+c9(s_K4$N-4tAMMvSTdDUSwxjqh93flJv#06T-}r@31o#bv~UK zOQd3v=->!jM$P4zG_}4^+P5yvS6l~ZCc$rXzpM519PCNDPiua2;eR7##$9ZC zt55=W;SStlH{f&l1g^24!2<#uWnaS*oaf&hhau=?58w&5%)e@Y72A5l06c?|t?Kgy zTea2W@0es>KymY;Q`!-ZMX+N!X3Tz#0L08f`h^V<556xlOUK076cQ) z0U{785Q2$-3HB)hE-Np(CtoXkYhDRnsA zsQvrWtSAyQRuCcDLjNg(kaoMjx>n(u4HMFlh^j=`R_I0Kpshfc=(_xavSt}zF6AuU z(n<$hA{>z~DGSPZ{h7jjQEQF#5b2ioMx+ytQw7;EeohoA##q$q?Tvcj=o-cj{pOe( z@C^Tb26^R)s)c< zk76n0h0Nn#QTc$)%b&^9+&Nj+Lz9QGNr|I5ZD#TizOFr-ay<2(R@J6HPQ&%f|G}y!o}NcZ{}2Ght^(CaLmtS6u%f>p|Xb zzP(T+?jgJ8{urBQ^8mnDF)D7ONY~GuCUus26qEJ{EWi{@fC2)$$G&48m-c3`q3l9J)AW z^60l}I~Rvtf$UK93KE_3=b3PBF_?NHRk1-n(M6-Ir(6H1uf^OeYtq% hQJOYHD!O=yR4(qaduTF`k9g=reuAl4J9YWf{{XNNATs~} From e174bac90d7c8d1a32e13b9d0f76a32a11644dd8 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 19:50:07 +0200 Subject: [PATCH 55/65] Updated typehints to work with python < 3.9 --- src/database_commander.py | 46 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/database_commander.py b/src/database_commander.py index 55475f43..6178b5de 100644 --- a/src/database_commander.py +++ b/src/database_commander.py @@ -3,7 +3,7 @@ import shutil from pathlib import Path import sqlite3 -from typing import Any +from typing import Any, Dict, List DATABASE_NAME = "Cocktail_database" DIRPATH = os.path.dirname(__file__) @@ -63,7 +63,7 @@ def get_all_recipes_properties(self): query = "SELECT ID, Name, Alkoholgehalt, Menge, Kommentar, Enabled FROM Rezepte" return self.handler.query_database(query) - def build_recipe_object(self) -> dict[str, dict]: + def build_recipe_object(self) -> Dict[str, Dict]: recipe_object = {} recipe_data = self.get_all_recipes_properties() for recipe in recipe_data: @@ -86,43 +86,43 @@ def get_recipe_ingredients_for_comment(self, recipe_name: str): WHERE Rezepte.Name = ? AND Zusammen.Hand = 1""" return self.handler.query_database(query, (recipe_name,)) - def get_enabled_recipes_id(self) -> list[int]: + def get_enabled_recipes_id(self) -> List[int]: recipe_data = self.get_all_recipes_properties() return [x[0] for x in recipe_data if x[5]] - def get_disabled_recipes_id(self) -> list[int]: + def get_disabled_recipes_id(self) -> List[int]: recipe_data = self.get_all_recipes_properties() return [x[0] for x in recipe_data if not x[5]] - def get_recipes_name(self) -> list[str]: + def get_recipes_name(self) -> List[str]: recipe_data = self.get_all_recipes_properties() return [x[1] for x in recipe_data] - def get_ingredients_at_bottles(self) -> list[str]: + def get_ingredients_at_bottles(self) -> List[str]: query = "SELECT Zutat_F FROM Belegung" result = self.handler.query_database(query) return [x[0] for x in result] - def get_ingredients_at_bottles_without_empty_ones(self) -> list[str]: + def get_ingredients_at_bottles_without_empty_ones(self) -> List[str]: data = self.get_ingredients_at_bottles() return [x for x in data if x != ""] - def get_ids_at_bottles(self) -> list[int]: + def get_ids_at_bottles(self) -> List[int]: query = "SELECT ID FROM Belegung" result = self.handler.query_database(query) return [x[0] for x in result] - def get_ingredient_names(self, condition_filter="") -> list[str]: + def get_ingredient_names(self, condition_filter="") -> List[str]: query = "SELECT Name FROM Zutaten" if condition_filter != "": query = f"{query} {condition_filter}" names = self.handler.query_database(query) return [x[0] for x in names] - def get_ingredient_names_hand(self) -> list[str]: + def get_ingredient_names_hand(self) -> List[str]: return self.get_ingredient_names("WHERE Hand = 1") - def get_ingredient_names_machine(self) -> list[str]: + def get_ingredient_names_machine(self) -> List[str]: return self.get_ingredient_names("WHERE Hand = 0") def get_ingredient_name_from_id(self, ingredient_id: int) -> str: @@ -132,7 +132,7 @@ def get_ingredient_name_from_id(self, ingredient_id: int) -> str: return data[0][0] return "" - def get_bottle_fill_levels(self) -> list[int]: + def get_bottle_fill_levels(self) -> List[int]: query = """SELECT Zutaten.Mengenlevel, Zutaten.Flaschenvolumen FROM Belegung LEFT JOIN Zutaten ON Zutaten.ID = Belegung.ID""" values = self.handler.query_database(query) @@ -145,7 +145,7 @@ def get_bottle_fill_levels(self) -> list[int]: levels.append(proportion) return levels - def get_ingredient_data(self, ingredient_name: str) -> dict[str, Any]: + def get_ingredient_data(self, ingredient_name: str) -> Dict[str, Any]: query = "SELECT ID, Name, Alkoholgehalt, Flaschenvolumen, Hand, Mengenlevel FROM Zutaten WHERE Name = ?" values = self.handler.query_database(query, (ingredient_name,)) if values: @@ -166,14 +166,14 @@ def get_bottle_usage(self, ingredient_id: int): return True return False - def get_recipe_usage_list(self, ingredient_id: int) -> list[str]: + def get_recipe_usage_list(self, ingredient_id: int) -> List[str]: query = """SELECT Rezepte.Name FROM Zusammen INNER JOIN Rezepte ON Rezepte.ID = Zusammen.Rezept_ID WHERE Zusammen.Zutaten_ID=?""" recipe_list = self.handler.query_database(query, (ingredient_id,)) return [recipe[0] for recipe in recipe_list] - def get_handadd_ids(self) -> list[int]: + def get_handadd_ids(self) -> List[int]: query = "SELECT ID FROM Vorhanden" result = self.handler.query_database(query) return [x[0] for x in result] @@ -189,13 +189,13 @@ def get_ingredients_seperated_by_handadd(self, recipe_id: int): machineadds.append(ingredient[3]) return handadds, machineadds - def get_multiple_recipe_names_from_ids(self, id_list: list[int]) -> list[str]: + def get_multiple_recipe_names_from_ids(self, id_list: List[int]) -> List[str]: questionmarks = ",".join(["?"] * len(id_list)) query = f"SELECT Name FROM Rezepte WHERE ID in ({questionmarks})" result = self.handler.query_database(query, id_list) return [x[0] for x in result] - def get_multiple_ingredient_ids_from_names(self, name_list: list[str]) -> list[int]: + def get_multiple_ingredient_ids_from_names(self, name_list: List[str]) -> List[int]: questionmarks = ",".join(["?"] * len(name_list)) query = f"SELECT ID FROM Zutaten WHERE Name in ({questionmarks})" result = self.handler.query_database(query, name_list) @@ -211,7 +211,7 @@ def get_consumption_data_lists_ingredients(self): data = self.handler.query_database(query) return self.convert_consumption_data(data) - def convert_consumption_data(self, data: list[list]): + def convert_consumption_data(self, data: List[List]): headers = [row[0] for row in data] resetable = [row[1] for row in data] lifetime = [row[2] for row in data] @@ -221,7 +221,7 @@ def get_enabled_status(self, recipe_name: str) -> int: query = "SELECT Enabled FROM Rezepte WHERE Name = ?" return self.handler.query_database(query, (recipe_name,))[0] - def get_available_ingredient_names(self) -> list[str]: + def get_available_ingredient_names(self) -> List[str]: query = """SELECT Zutaten.Name FROM Zutaten INNER JOIN Vorhanden ON Vorhanden.ID = Zutaten.ID""" data = self.handler.query_database(query) @@ -242,7 +242,7 @@ def get_ingredient_bottle_and_level_by_name(self, ingredient_name): return 0, 0 # set (update) commands - def set_bottleorder(self, ingredient_names: list[str]): + def set_bottleorder(self, ingredient_names: List[str]): for i, ingredient in enumerate(ingredient_names): bottle = i + 1 query = """UPDATE OR IGNORE Belegung @@ -252,7 +252,7 @@ def set_bottleorder(self, ingredient_names: list[str]): searchtuple = (ingredient, ingredient, bottle) self.handler.query_database(query, searchtuple) - def set_bottle_volumelevel_to_max(self, boolean_list: list[bool]): + def set_bottle_volumelevel_to_max(self, boolean_list: List[bool]): query = """UPDATE OR IGNORE Zutaten Set Mengenlevel = Flaschenvolumen WHERE ID = (SELECT ID FROM Belegung WHERE Flasche = ?)""" @@ -286,7 +286,7 @@ def set_ingredient_consumption(self, ingredient_name: str, ingredient_consumptio searchtuple = (ingredient_consumption, ingredient_consumption, ingredient_consumption, ingredient_name) self.handler.query_database(query, searchtuple) - def set_multiple_ingredient_consumption(self, ingredient_name_list: list[str], ingredient_consumption_list: list[int]): + def set_multiple_ingredient_consumption(self, ingredient_name_list: List[str], ingredient_consumption_list: List[int]): for ingredient_name, ingredient_consumption in zip(ingredient_name_list, ingredient_consumption_list): self.set_ingredient_consumption(ingredient_name, ingredient_consumption) @@ -325,7 +325,7 @@ def insert_recipe_data(self, recipe_id: int, ingredient_id: int, ingredient_volu searchtuple = (recipe_id, ingredient_id, ingredient_volume, isalcoholic, hand_add) self.handler.query_database(query, searchtuple) - def insert_multiple_existing_handadd_ingredients_by_name(self, ingredient_names: list[str]): + def insert_multiple_existing_handadd_ingredients_by_name(self, ingredient_names: List[str]): ingredient_id = self.get_multiple_ingredient_ids_from_names(ingredient_names) questionmarks = ",".join(["(?)"] * len(ingredient_id)) query = f"INSERT INTO Vorhanden(ID) VALUES {questionmarks}" From c0c10c9fdb053950827e93a662f4c7a59c733317 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 19:52:41 +0200 Subject: [PATCH 56/65] Further adjustments for python < 3.9 --- src/rpi_controller.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/rpi_controller.py b/src/rpi_controller.py index 70f5d59b..4d8e66aa 100644 --- a/src/rpi_controller.py +++ b/src/rpi_controller.py @@ -1,4 +1,5 @@ import time +from typing import List from PyQt5.QtWidgets import qApp from config.config_manager import shared @@ -32,19 +33,19 @@ def clean_pumps(self): qApp.processEvents() self.close_pinlist(active_pins) - def make_cocktail(self, w, bottle_list: list[int], volume_list: list[float], labelchange=""): + def make_cocktail(self, w, bottle_list: List[int], volume_list: List[float], labelchange=""): """RPI Logic to prepare the cocktail. Calculates needed time for each slot according to data and config. Updates Progressbar status. Returns data for DB updates. Args: w (QtMainWindow): MainWindow Object - bottle_list (list[int]): Number of bottles to be used - volume_list (list[float]): Corresponding Volumens needed of bottles + bottle_list (List[int]): Number of bottles to be used + volume_list (List[float]): Corresponding Volumens needed of bottles labelchange (str, optional): Option to change the display text of Progress Screen. Defaults to "". Returns: - tuple(list[int], float, float): Consumption of each bottle, taken time, max needed time + tuple(List[int], float, float): Consumption of each bottle, taken time, max needed time """ # Only shwo team dialog if it is enabled if self.USE_TEAMS: @@ -88,20 +89,20 @@ def close_pin(self, pin: int, current_time: float): GPIO.output(pin, 1) print(f"{current_time}s: Pin number <{pin}> is closed") - def activate_pinlist(self, pinlist: list[int]): + def activate_pinlist(self, pinlist: List[int]): print(f"Opening Pins: {pinlist}") if not self.devenvironment: for pin in pinlist: GPIO.setup(pin, 0) GPIO.output(pin, 0) - def close_pinlist(self, pinlist: list[int]): + def close_pinlist(self, pinlist: List[int]): print(f"Closing Pins: {pinlist}") if not self.devenvironment: for pin in pinlist: GPIO.output(pin, 1) - def consumption_print(self, consumption: list[float], current_time: float, max_time: float, interval=1): + def consumption_print(self, consumption: List[float], current_time: float, max_time: float, interval=1): if current_time % interval == 0: print( f"Making Cocktail, {current_time}/{max_time} s:\tThe consumption is currently {[round(x) for x in consumption]}") From be8cbca3ade03784a682115478aa885fff0dc899 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 20:31:49 +0200 Subject: [PATCH 57/65] Added shell and .desktop scripts --- cocktail.desktop | 5 +++++ launcher.sh | 10 ++++++++++ readme.md | 17 +++++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 cocktail.desktop create mode 100644 launcher.sh diff --git a/cocktail.desktop b/cocktail.desktop new file mode 100644 index 00000000..39e9725a --- /dev/null +++ b/cocktail.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=CocktailScreen +NoDisplay=false +Exec=/usr/bin/lxterminal -e /home/pi/launcher.sh \ No newline at end of file diff --git a/launcher.sh b/launcher.sh new file mode 100644 index 00000000..71f222d2 --- /dev/null +++ b/launcher.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# launcher.sh for both, maker and dashboard +# uncomment / comment the according lines + +# launcher.sh for dashboard +# no need for sudo if there were no Numpy import errors +# sudo python3 /home/pi/Cocktailmaker_AW/dashboard/qt-app/main.py + +# launcher.sh for cocktailmaker +python3 /home/pi/Cocktailmaker_AW/runme.py \ No newline at end of file diff --git a/readme.md b/readme.md index 31001901..be9cedf3 100644 --- a/readme.md +++ b/readme.md @@ -24,6 +24,7 @@ - [PyQt can't be Installed](#pyqt-cant-be-installed) - [Numpy Import Error at Matplotlib Import](#numpy-import-error-at-matplotlib-import) - [How to get the GUI Running on Startup](#how-to-get-the-gui-running-on-startup) + - [The GUI on the RPi Looks Different than on the Screenshots](#the-gui-on-the-rpi-looks-different-than-on-the-screenshots) - [Advanced Topics](#advanced-topics) - [Microservices](#microservices) - [Dashboard with Teams](#dashboard-with-teams) @@ -181,7 +182,7 @@ You probably need to run `sudo apt install python3-pyqt5` instead of `pip instal ### Numpy Import Error at Matplotlib Import -Try first running `pip3 install -U numpy` and `sudo apt install libatlas3-base`. If it is still not fixed, try uninstalling and installing numpy / matplotlib again. +Try first running `pip3 install -U numpy` and `sudo apt install libatlas3-base`. If it is still not fixed, try uninstalling and installing numpy / matplotlib again. If really nothing else works try `sudo pip3 install -U numpy`, then you will probably need to run the python file with root priviledge as well, which may result in another GUI style used by the system. ### How to get the GUI Running on Startup @@ -198,15 +199,27 @@ Exec=/usr/bin/lxterminal -e /home/pi/launcher.sh ```bash #!/bin/bash # launcher.sh for dashboard +# no need for sudo if there were no Numpy import errors sudo python3 /home/pi/Cocktailmaker_AW/dashboard/qt-app/main.py ``` ```bash #!/bin/bash # launcher.sh for cocktailmaker -sudo python3 /home/pi/Cocktailmaker_AW/runme.py +python3 /home/pi/Cocktailmaker_AW/runme.py ``` +If your setup is equal to mine (Raspberry Pi, Maker GitHub cloned to `/home/pi/` folder) you can also just copy the files and comment/uncomment within the launcher.sh to save some typing: + +```bash +cp /home/pi/Cocktailmaker_AW/launcher.sh /home/pi/ +cp /home/pi/Cocktailmaker_AW/cocktail.desktop /etc/xdg/autostart/ +``` + +### The GUI on the RPi Looks Different than on the Screenshots + +I've noticed when running as root (sudo python3) and running as the pi user (python3) by default the pi will use different GUI ressources. Using the pi user will result in the shown interfaces at the cocktailmaker (and the program should work without root privilege). Setting the XDG_RUNTIME_DIR to use the qt5ct plugin may also work but is untested. + # Advanced Topics ## Microservices From 3df65cdc46a7413280394e3f1e129df200bb933a Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 20:42:02 +0200 Subject: [PATCH 58/65] Added toubleshooting for sh not executing --- readme.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/readme.md b/readme.md index be9cedf3..325bb44a 100644 --- a/readme.md +++ b/readme.md @@ -216,6 +216,14 @@ cp /home/pi/Cocktailmaker_AW/launcher.sh /home/pi/ cp /home/pi/Cocktailmaker_AW/cocktail.desktop /etc/xdg/autostart/ ``` +If there are any problems with the lxterminal window opening and instant closing, check the rights of the shell file, it needs executable (x) rights, otherwise use `chmod` to give x-rights: + +```bash +sudo chmod +x /home/pi/launcher.sh +# or +sudo chmod 755 /home/pi/launcher.sh +``` + ### The GUI on the RPi Looks Different than on the Screenshots I've noticed when running as root (sudo python3) and running as the pi user (python3) by default the pi will use different GUI ressources. Using the pi user will result in the shown interfaces at the cocktailmaker (and the program should work without root privilege). Setting the XDG_RUNTIME_DIR to use the qt5ct plugin may also work but is untested. From 34270b05d322cb9ca13008ebd2305bcacd1349ba Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 21:17:30 +0200 Subject: [PATCH 59/65] Fixed bug not importing module properly --- src/rpi_controller.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/rpi_controller.py b/src/rpi_controller.py index 4d8e66aa..00c69e84 100644 --- a/src/rpi_controller.py +++ b/src/rpi_controller.py @@ -6,17 +6,20 @@ from config.config_manager import ConfigManager +try: + from RPi import GPIO + GPIO.setmode(GPIO.BCM) + DEV = False +except ModuleNotFoundError: + DEV = True + + class RpiController(ConfigManager): """Controler Class for all RPi related GPIO routines """ def __init__(self): - try: - import RPi.GPIO as GPIO - - GPIO.setmode(GPIO.BCM) - self.devenvironment = False - except ModuleNotFoundError: - self.devenvironment = True + self.devenvironment = DEV + print(f"Devenvironment is {self.devenvironment}") def clean_pumps(self): """Clean the pumps for the defined time in the config. From 4eeb5e896f7d44301ead057d0690cdc27a61d995 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 21:19:42 +0200 Subject: [PATCH 60/65] Added instructions for installing docker --- readme.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/readme.md b/readme.md index 325bb44a..8551c6b4 100644 --- a/readme.md +++ b/readme.md @@ -29,6 +29,7 @@ - [Microservices](#microservices) - [Dashboard with Teams](#dashboard-with-teams) - [Usage of Services](#usage-of-services) + - [Installing Docker](#installing-docker) - [Development](#development) - [Program Schema](#program-schema) - [Pull Requests and Issues](#pull-requests-and-issues) @@ -262,6 +263,22 @@ docker-compose up -d This will handle the setup of all docker services. You will have to rename the `.env.example` file to `.env` and enter the needed secrets there for the container to work fully. +## Installing Docker + +tl;dr: Just run these commands in sequence on the pi and reboot after the first half. + +```bash +sudo apt-get update && sudo apt-get upgrade +curl -sSL https://get.docker.com | sh +sudo usermod -aG docker ${USER} +# reboot here +sudo apt-get install libffi-dev libssl-dev +sudo pip3 install docker-compose +sudo systemctl enable docker +# tesing if it works +docker run hello-world +``` + # Development ## Program Schema From 05895d1f973d84024c9a0b626a7a6b7c3daee297 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Mon, 18 Oct 2021 21:56:50 +0200 Subject: [PATCH 61/65] Fiox that window is on top at RPi --- src_ui/setup_team_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src_ui/setup_team_window.py b/src_ui/setup_team_window.py index faa7ecfc..c28a0942 100644 --- a/src_ui/setup_team_window.py +++ b/src_ui/setup_team_window.py @@ -21,6 +21,7 @@ def __init__(self, parent=None): self.mainscreen = parent if not self.mainscreen.DEVENVIRONMENT: self.setCursor(Qt.BlankCursor) + self.move(0, 0) def set_team(team: str): shared.selected_team = team From eba9eef4611d2550b187b7ee4c94d4018bc38e38 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Tue, 19 Oct 2021 10:19:01 +0200 Subject: [PATCH 62/65] Adjusted changelog --- changelog.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changelog.txt b/changelog.txt index c3b97361..5f5cb560 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,13 @@ +--------------- 2021_10_19 --------------------------------------------------------------------------------------------------------- +- release of v1.2 +- Introduced teams to have a competetive point +- Added Dashboard for display of teams +- Added API Service for teams +- Introduced type hints a many places, more docs +- Some refactoring +- Removed global vars and switched to Shared class +- Switched to default db and the creation of local db, that git won't overwrite local db + --------------- 2020_12_20 --------------------------------------------------------------------------------------------------------- - Introduced microservice to post tasks to - This includes an endpoint to make a post of the cocktail name and date to an webhook / endpoint From 400a074503dc1c3040c6dec81edee188571bb441 Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Wed, 20 Oct 2021 15:05:54 +0200 Subject: [PATCH 63/65] Introduced safer logic to not fill memory --- dashboard/qt-app/setup_leaderboard.py | 27 ++++++++++++++------------- dashboard/qt-app/waffle.py | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/dashboard/qt-app/setup_leaderboard.py b/dashboard/qt-app/setup_leaderboard.py index 1e3d8e31..d9afa373 100644 --- a/dashboard/qt-app/setup_leaderboard.py +++ b/dashboard/qt-app/setup_leaderboard.py @@ -4,6 +4,7 @@ import time import matplotlib from mainwindow import Ui_Leaderboard +# import datetime from PyQt5.QtCore import Qt, QRunnable, pyqtSlot, QObject, pyqtSignal, QThreadPool from PyQt5.QtWidgets import QMainWindow, qApp @@ -22,29 +23,29 @@ def __init__(self): self.setupUi(self) self.setWindowFlags(Qt.FramelessWindowHint) - self.selectbtn.clicked.connect(self.select) + self.selectbtn.clicked.connect(lambda: self.update(getnext=True)) self.options = cycle([1, 2, 3, 4]) self.curr_option = next(self.options) - fig = generate_figure(self.curr_option) - self.canvas = FigureCanvas(fig) + self.fig = generate_figure(self.curr_option) + self.canvas = FigureCanvas(self.fig) self.horizontalLayout.addWidget(self.canvas) # Spinning up a threadpool to constantly update self.threadpool = QThreadPool() self.start_worker() - def select(self): + def update(self, getnext=False): + # print(f"{datetime.datetime.now().strftime('%H:%M:%S')}: Update triggered with next {getnext}") + self.canvas.figure.get_axes().clear() self.canvas.deleteLater() - self.curr_option = next(self.options) - self.canvas = FigureCanvas(generate_figure(self.curr_option)) + if getnext: + self.curr_option = next(self.options) + self.fig = generate_figure(self.curr_option) + self.canvas = FigureCanvas(self.fig) self.horizontalLayout.addWidget(self.canvas) - - def update(self): - self.canvas.deleteLater() - self.canvas = FigureCanvas(generate_figure(self.curr_option)) - self.horizontalLayout.addWidget(self.canvas) - self.start_worker() + if not getnext: + self.start_worker() def start_worker(self): """Starts a Worker with the timer, updates plot afterwards""" @@ -53,7 +54,7 @@ def start_worker(self): self.threadpool.start(worker) def update_intervall(self): - time.sleep(10) + time.sleep(15) class WorkerSignals(QObject): diff --git a/dashboard/qt-app/waffle.py b/dashboard/qt-app/waffle.py index a8dddd9c..c4f55751 100644 --- a/dashboard/qt-app/waffle.py +++ b/dashboard/qt-app/waffle.py @@ -89,7 +89,7 @@ def generate_figure(datatype: int): dims = generate_dimensions(sum(data.values()), count) # close current (old) figure, to avoid memory leak # this is needed because old figures will keep open until explicitly closed - plt.close() + plt.close('all') fig = plt.figure( FigureClass=Waffle, **dims, From aa3c3b11e4b8209fd202ceb07a1231cec6abc19f Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Thu, 21 Oct 2021 14:49:36 +0200 Subject: [PATCH 64/65] Added image name for better organisation --- dashboard/docker-compose.yaml | 1 + docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/dashboard/docker-compose.yaml b/dashboard/docker-compose.yaml index 8061d14d..7dbaa4a9 100644 --- a/dashboard/docker-compose.yaml +++ b/dashboard/docker-compose.yaml @@ -2,6 +2,7 @@ version: '3' services: backend: + image: cocktail-dashboard-backend container_name: cocktail-dashboard-backend restart: always build: backend diff --git a/docker-compose.yml b/docker-compose.yml index 47f7b0cd..2c273b1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ version: '3.8' services: api-service: + image: cocktail-microservice container_name: cocktail-microservice restart: always build: ./microservice/ From 5996b8d8bc67e2abfa2cd94a5afe5cdf3f68080b Mon Sep 17 00:00:00 2001 From: Andre Wohnsland Date: Thu, 21 Oct 2021 19:15:11 +0200 Subject: [PATCH 65/65] Added saving and later resending of failed cocktails --- .gitignore | 3 ++- microservice/app.py | 9 ++++++- microservice/database.py | 50 +++++++++++++++++++++++++++++++++++ microservice/querry_sender.py | 24 +++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 microservice/database.py create mode 100644 microservice/querry_sender.py diff --git a/.gitignore b/.gitignore index fc7d99b4..26a7a42a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ __pycache__ .env .venv team.db -Cocktail_database.db \ No newline at end of file +Cocktail_database.db +failed_data.db \ No newline at end of file diff --git a/microservice/app.py b/microservice/app.py index 98378832..17c2d1c3 100644 --- a/microservice/app.py +++ b/microservice/app.py @@ -4,10 +4,14 @@ import logging import json from threading import Thread +from typing import Dict import requests from dotenv import load_dotenv from flask import Flask, request, abort, jsonify + +from querry_sender import try_send_querry_data from email_sender import send_mail +from database import DatabaseHandler load_dotenv() @@ -22,12 +26,14 @@ def welcome(): @app.route("/hookhandler/cocktail", methods=["POST"]) def post_cocktail_hook(): - def post_to_hook(url, payload, headers): + def post_to_hook(url: str, payload: str, headers: Dict): try: req = requests.post(url, data=payload, headers=headers) app.logger.info(f"{req.status_code}: Posted to webhook with payload: {payload}") except requests.exceptions.ConnectionError: app.logger.error("Could not connect to the webhook for the cocktail!") + db_handler = DatabaseHandler() + db_handler.save_failed_post(payload) if not request.json or not "cocktailname" in request.json: abort(400) @@ -54,4 +60,5 @@ def post_file_with_mail(): if __name__ == "__main__": + try_send_querry_data() app.run(host="0.0.0.0", port=os.getenv("PORT")) diff --git a/microservice/database.py b/microservice/database.py new file mode 100644 index 00000000..6f8aea2f --- /dev/null +++ b/microservice/database.py @@ -0,0 +1,50 @@ +import os +from pathlib import Path +import sqlite3 + +DATABASE_NAME = "failed_data" +DIRPATH = os.path.dirname(__file__) + + +class DatabaseHandler: + """Handler Class for Connecting and querring Databases""" + + def __init__(self): + self.database_path = os.path.join(DIRPATH, f"{DATABASE_NAME}.db") + if not Path(self.database_path).exists(): + print("creating Database") + self.create_tables() + self.database = sqlite3.connect(self.database_path) + self.cursor = self.database.cursor() + + def save_failed_post(self, payload: str): + sql = "INSERT INTO Querry(Payload) VALUES(?)" + self.query_database(sql, (payload,)) + + def get_failed_data(self): + sql = "SELECT * FROM Querry ORDER BY ID ASC" + return self.query_database(sql) + + def delete_failed_by_id(self, data_id: int): + sql = "DELETE FROM Querry WHERE ID = ?" + self.query_database(sql, (data_id,)) + + def query_database(self, sql: str, serachtuple=()): + self.cursor.execute(sql, serachtuple) + if sql[0:6].lower() == "select": + result = self.cursor.fetchall() + else: + self.database.commit() + result = [] + return result + + def create_tables(self): + self.database = sqlite3.connect(self.database_path) + self.cursor = self.database.cursor() + self.cursor.execute( + """CREATE TABLE IF NOT EXISTS Querry( + ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + Payload TEXT NOT NULL);""" + ) + self.database.commit() + self.database.close() diff --git a/microservice/querry_sender.py b/microservice/querry_sender.py new file mode 100644 index 00000000..31df628e --- /dev/null +++ b/microservice/querry_sender.py @@ -0,0 +1,24 @@ +import os +import requests +from database import DatabaseHandler + + +def try_send_querry_data(): + db_handler = DatabaseHandler() + failed_data = db_handler.get_failed_data() + headers = {"content-type": "application/json"} + url = os.getenv("HOOK_ENDPOINT") + # Return if nothing to do + if not failed_data: + return + # Else try to send all remaining data + print("Got some not send data, trying to send ...") + for send_id, data in failed_data: + try: + res = requests.post(url, data=data, headers=headers) + print(f"Code: {res.status_code}, Payload: {data}") + except requests.exceptions.ConnectionError: + return + # if send successfully, delete this entry + else: + db_handler.delete_failed_by_id(send_id)