From 4aad5b5bbbac52d38bb5d32dbba85ba86d8b142d Mon Sep 17 00:00:00 2001 From: jasonBirchall Date: Tue, 10 Dec 2024 12:50:34 +0000 Subject: [PATCH] :sparkles: Add initial grafana dashboard application --- .dockerignore | 70 ++ Dockerfile | 23 + Pipfile | 18 + Pipfile.lock | 769 ++++++++++++++++++ app/__init__.py | 0 app/app.py | 168 ++++ app/assets/main.css | 11 + app/config/__init__.py | 0 app/config/app_config.py | 43 + app/config/logging_config.py | 10 + app/config/routes_config.py | 9 + app/routes/api_route.py | 28 + app/routes/index_route.py | 11 + app/run.py | 17 + app/services/__init__.py | 0 app/services/database_service.py | 61 ++ app/services/figure_service.py | 334 ++++++++ docker-compose-grafana.yaml | 46 ++ docker-compose.yaml | 44 + grafana/Dockerfile | 20 + grafana/dashboards/plotly-dashboard-copy.json | 687 ++++++++++++++++ grafana/grafana.ini | 26 + grafana/provisioning/dashboards/dashboard.yml | 25 + .../provisioning/datasources/datasource.yml | 40 + helm/kubera/.helmignore | 23 + helm/kubera/Chart.yaml | 24 + helm/kubera/templates/_helpers.tpl | 51 ++ helm/kubera/templates/deployment.yaml | 45 + helm/kubera/templates/ingress.yaml | 29 + helm/kubera/templates/service.yaml | 11 + helm/kubera/values-prod.yaml | 8 + 31 files changed, 2651 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 app/__init__.py create mode 100644 app/app.py create mode 100644 app/assets/main.css create mode 100644 app/config/__init__.py create mode 100644 app/config/app_config.py create mode 100644 app/config/logging_config.py create mode 100644 app/config/routes_config.py create mode 100644 app/routes/api_route.py create mode 100644 app/routes/index_route.py create mode 100644 app/run.py create mode 100644 app/services/__init__.py create mode 100644 app/services/database_service.py create mode 100644 app/services/figure_service.py create mode 100644 docker-compose-grafana.yaml create mode 100644 docker-compose.yaml create mode 100644 grafana/Dockerfile create mode 100644 grafana/dashboards/plotly-dashboard-copy.json create mode 100644 grafana/grafana.ini create mode 100644 grafana/provisioning/dashboards/dashboard.yml create mode 100644 grafana/provisioning/datasources/datasource.yml create mode 100644 helm/kubera/.helmignore create mode 100644 helm/kubera/Chart.yaml create mode 100644 helm/kubera/templates/_helpers.tpl create mode 100644 helm/kubera/templates/deployment.yaml create mode 100644 helm/kubera/templates/ingress.yaml create mode 100644 helm/kubera/templates/service.yaml create mode 100644 helm/kubera/values-prod.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..13dd5e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,70 @@ +Dockerfile +.git/ +.github/ + +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +*.log + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build + +.webassets-cache + +# Virtualenvs +env +env* +venv + +# intellij +.idea/ +*.ipr +*.iml +*.iws + +.DS_Store + +# node +node_modules/ + +.sass-cache/ + +docs/ +README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ed70734 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12.0-slim + +RUN groupadd -r user && useradd -r -g user 1051 + +WORKDIR /home/operations-engineering-kubera + +COPY Pipfile Pipfile +COPY Pipfile.lock Pipfile.lock +COPY app app +COPY example-data data +COPY data/production production + +RUN pip3 install --no-cache-dir pipenv +RUN pipenv install --system --deploy --ignore-pipfile + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +EXPOSE 4567 + +USER 1051 + +ENTRYPOINT ["gunicorn", "--bind=0.0.0.0:4567", "app.run:app()"] diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..1c5724b --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +authlib = "==1.3.1" +dash_auth = "==2.3.0" +flask = "==3.0.0" +gunicorn = "==21.2.0" +psycopg2-binary = "2.9.9" +plotly = "==5.21.0" +dash = "==2.16.1" +pandas = "==2.2.2" +statsmodels = "==0.14.1" + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..7969cc7 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,769 @@ +{ + "_meta": { + "hash": { + "sha256": "5050b4dfb9dcbd969ba1598fa86196e8f44624500c43780497968ac83268840e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "authlib": { + "hashes": [ + "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", + "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.3.1" + }, + "blinker": { + "hashes": [ + "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", + "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" + ], + "markers": "python_version >= '3.8'", + "version": "==1.8.2" + }, + "certifi": { + "hashes": [ + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.7.4" + }, + "cffi": { + "hashes": [ + "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", + "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab", + "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499", + "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058", + "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693", + "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb", + "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", + "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", + "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2", + "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401", + "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4", + "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b", + "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", + "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f", + "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c", + "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", + "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa", + "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", + "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", + "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2", + "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", + "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e", + "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", + "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82", + "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", + "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759", + "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc", + "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", + "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf", + "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932", + "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", + "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29", + "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206", + "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2", + "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c", + "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c", + "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", + "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a", + "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", + "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", + "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", + "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", + "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", + "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", + "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", + "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", + "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", + "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", + "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", + "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42", + "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", + "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", + "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d", + "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", + "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4", + "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", + "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b", + "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", + "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e", + "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", + "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3", + "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", + "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", + "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", + "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", + "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb", + "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "cryptography": { + "hashes": [ + "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709", + "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069", + "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2", + "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", + "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", + "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70", + "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778", + "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", + "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", + "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf", + "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", + "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f", + "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947", + "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", + "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc", + "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", + "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66", + "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", + "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f", + "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", + "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", + "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", + "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", + "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1", + "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", + "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", + "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0" + ], + "markers": "python_version >= '3.7'", + "version": "==43.0.0" + }, + "dash": { + "hashes": [ + "sha256:8a9d2a618e415113c0b2a4d25d5dc4df5cb921f733b33dde75559db2316b1df1", + "sha256:b2871d6b8d4c9dfd0a64f89f22d001c93292910b41d92d9ff2bb424a28283976" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.16.1" + }, + "dash-auth": { + "hashes": [ + "sha256:2fa72d4ee128b4f9cf0c958157a986ffcd0b93d43b54d64e3cef5976ab7e0abe", + "sha256:72df43248c15e121f8d5d710981e880deb2df546b564f2951a10ca50d7d92f6d" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.3.0" + }, + "dash-core-components": { + "hashes": [ + "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346", + "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee" + ], + "version": "==2.0.0" + }, + "dash-html-components": { + "hashes": [ + "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50", + "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63" + ], + "version": "==2.0.0" + }, + "dash-table": { + "hashes": [ + "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308", + "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9" + ], + "version": "==5.0.0" + }, + "flask": { + "hashes": [ + "sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638", + "sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "index": "pypi", + "markers": "python_version >= '3.5'", + "version": "==21.2.0" + }, + "idna": { + "hashes": [ + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + ], + "markers": "python_version >= '3.5'", + "version": "==3.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67", + "sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364" + ], + "markers": "python_version >= '3.8'", + "version": "==8.3.0" + }, + "itsdangerous": { + "hashes": [ + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.0" + }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, + "nest-asyncio": { + "hashes": [ + "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", + "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c" + ], + "markers": "python_version >= '3.5'", + "version": "==1.6.0" + }, + "numpy": { + "hashes": [ + "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", + "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", + "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", + "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", + "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", + "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", + "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", + "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", + "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", + "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", + "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", + "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", + "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", + "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", + "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", + "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", + "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", + "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", + "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", + "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", + "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", + "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", + "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", + "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", + "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", + "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", + "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", + "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", + "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", + "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", + "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", + "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", + "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", + "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", + "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", + "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" + ], + "markers": "python_version >= '3.12'", + "version": "==1.26.4" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pandas": { + "hashes": [ + "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863", + "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2", + "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", + "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", + "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", + "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", + "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51", + "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", + "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08", + "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", + "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4", + "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921", + "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", + "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", + "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0", + "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", + "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99", + "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", + "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd", + "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce", + "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57", + "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", + "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", + "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a", + "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238", + "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", + "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772", + "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", + "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.2.2" + }, + "patsy": { + "hashes": [ + "sha256:19056886fd8fa71863fa32f0eb090267f21fb74be00f19f5c70b2e9d76c883c6", + "sha256:95c6d47a7222535f84bff7f63d7303f2e297747a598db89cf5c67f0c0c7d2cdb" + ], + "version": "==0.5.6" + }, + "plotly": { + "hashes": [ + "sha256:69243f8c165d4be26c0df1c6f0b7b258e2dfeefe032763404ad7e7fb7d7c2073", + "sha256:a33f41fd5922e45b2b253f795b200d14452eb625790bb72d0a72cf1328a6abbf" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.21.0" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", + "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", + "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", + "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", + "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", + "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", + "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", + "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", + "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", + "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", + "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", + "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", + "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", + "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", + "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", + "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", + "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", + "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", + "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", + "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", + "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", + "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", + "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", + "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", + "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", + "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", + "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", + "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", + "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", + "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", + "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", + "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", + "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", + "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", + "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", + "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", + "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", + "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", + "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", + "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", + "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", + "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", + "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", + "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", + "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", + "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", + "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", + "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", + "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", + "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", + "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", + "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", + "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", + "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", + "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", + "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", + "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", + "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", + "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", + "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", + "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", + "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", + "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", + "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", + "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", + "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", + "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", + "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", + "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", + "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", + "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", + "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.9.9" + }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "pytz": { + "hashes": [ + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + ], + "version": "==2024.1" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "retrying": { + "hashes": [ + "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e", + "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35" + ], + "version": "==1.3.4" + }, + "scipy": { + "hashes": [ + "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0", + "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7", + "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d", + "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0", + "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20", + "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc", + "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0", + "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159", + "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6", + "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1", + "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e", + "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f", + "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484", + "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf", + "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74", + "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8", + "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14", + "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86", + "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359", + "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b", + "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4", + "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9", + "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb", + "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209", + "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb" + ], + "markers": "python_version >= '3.10'", + "version": "==1.14.0" + }, + "setuptools": { + "hashes": [ + "sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0", + "sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864" + ], + "markers": "python_version >= '3.8'", + "version": "==73.0.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "statsmodels": { + "hashes": [ + "sha256:04293890f153ffe577e60a227bd43babd5f6c1fc50ea56a3ab1862ae85247a95", + "sha256:0a8aae75a2e08ebd990e5fa394f8e32738b55785cb70798449a3f4207085e667", + "sha256:0d5373d176239993c095b00d06036690a50309a4e00c2da553b65b840f956ae6", + "sha256:2260efdc1ef89f39c670a0bd8151b1d0843567781bcafec6cda0534eb47a94f6", + "sha256:2de2b97413913d52ad6342dece2d653e77f78620013b7705fad291d4e4266ccb", + "sha256:3e70a2e93d54d40b2cb6426072acbc04f35501b1ea2569f6786964adde6ca572", + "sha256:43af9c0b07c9d72f275cf14ea54a481a3f20911f0b443181be4769def258fdeb", + "sha256:44ca8cb88fa3d3a4ffaff1fb8eb0e98bbf83fc936fcd9b9eedee258ecc76696a", + "sha256:4fe0a60695952b82139ae8750952786a700292f9e0551d572d7685070944487b", + "sha256:5385e22e72159a09c099c4fb975f350a9f3afeb57c1efce273b89dcf1fe44c0f", + "sha256:709bfcef2dbe66f705b17e56d1021abad02243ee1a5d1efdb90f9bad8b06a329", + "sha256:7562cb18a90a114f39fab6f1c25b9c7b39d9cd5f433d0044b430ca9d44a8b52c", + "sha256:a16975ab6ad505d837ba9aee11f92a8c5b49c4fa1ff45b60fe23780b19e5705e", + "sha256:a532dfe899f8b6632cd8caa0b089b403415618f51e840d1817a1e4b97e200c73", + "sha256:ab3a73d16c0569adbba181ebb967e5baaa74935f6d2efe86ac6fc5857449b07d", + "sha256:b0f727fe697f6406d5f677b67211abe5a55101896abdfacdb3f38410405f6ad8", + "sha256:b3abaca4b963259a2bf349c7609cfbb0ce64ad5fb3d92d6f08e21453e4890248", + "sha256:b6838ac6bdb286daabb5e91af90fd4258f09d0cec9aace78cc441cb2b17df428", + "sha256:b69a63ad6c979a6e4cde11870ffa727c76a318c225a7e509f031fbbdfb4e416a", + "sha256:bc0351d279c4e080f0ce638a3d886d312aa29eade96042e3ba0a73771b1abdfb", + "sha256:bc43765710099ca6a942b5ffa1bac7668965052542ba793dd072d26c83453572", + "sha256:bf293ada63b2859d95210165ad1dfcd97bd7b994a5266d6fbeb23659d8f0bf68", + "sha256:c008e16096f24f0514e53907890ccac6589a16ad6c81c218f2ee6752fdada555", + "sha256:c0564d92cb05b219b4538ed09e77d96658a924a691255e1f7dd23ee338df441b", + "sha256:c3420f88289c593ba2bca33619023059c476674c160733bd7d858564787c83d3", + "sha256:e278fe74da5ed5e06c11a30851eda1af08ef5af6be8507c2c45d2e08f7550dde", + "sha256:eefa5bcff335440ee93e28745eab63559a20cd34eea0375c66d96b016de909b3", + "sha256:f32a7cd424cf33304a54daee39d32cccf1d0265e652c920adeaeedff6d576457", + "sha256:f8c30181c084173d662aaf0531867667be2ff1bee103b84feb64f149f792dbd2" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.14.1" + }, + "tenacity": { + "hashes": [ + "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", + "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539" + ], + "markers": "python_version >= '3.8'", + "version": "==9.0.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "tzdata": { + "hashes": [ + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + ], + "markers": "python_version >= '2'", + "version": "==2024.1" + }, + "urllib3": { + "hashes": [ + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.2" + }, + "werkzeug": { + "hashes": [ + "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", + "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.3" + }, + "zipp": { + "hashes": [ + "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", + "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d" + ], + "markers": "python_version >= '3.8'", + "version": "==3.20.0" + } + }, + "develop": {} +} diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..44aabf3 --- /dev/null +++ b/app/app.py @@ -0,0 +1,168 @@ +import logging + +from dash import Dash, dcc, html +from dash_auth import OIDCAuth, add_public_routes +from flask import Flask + +from app.config.app_config import app_config +from app.config.logging_config import configure_logging +from app.config.routes_config import configure_routes +from app.services.database_service import DatabaseService +from app.services.figure_service import FigureService + +logger = logging.getLogger(__name__) + + +def create_app() -> Flask: + configure_logging(app_config.logging_level) + + logger.info("Starting app...") + + server = Flask(__name__) + + server.database_service = DatabaseService() + server.figure_service = FigureService(server.database_service) + + configure_routes(server) + + logger.info("Populating stub data...") + server.database_service.create_indicators_table() + server.database_service.clean_stubbed_indicators_table() + + app = Dash(__name__, server=server, url_base_pathname="/dashboard/") + app.title = "⚙️ SLO dashboard" + app.layout = create_dashboard(server.figure_service) + + if app_config.auth_enabled: + auth = OIDCAuth( + app, + secret_key=app_config.flask.app_secret_key, + force_https_callback=True, + secure_session=True, + ) + add_public_routes(app, routes=["/api/indicator/add"]) + auth.register_provider( + "idp", + token_endpoint_auth_method="client_secret_post", + client_id=app_config.auth0.client_id, + client_secret=app_config.auth0.client_secret, + server_metadata_url=f"https://{app_config.auth0.domain}/.well-known/openid-configuration", + ) + logger.info("Running app...") + + return app.server + + +def create_dashboard(figure_service: FigureService): + def dashboard(): + return html.Div( + children=[ + html.H1("🤩 Live Data 🤩"), + dcc.Graph( + figure=figure_service.get_number_of_repositories_with_standards_label_dashboard(), + style={ + "width": "100%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_support_stats_year_to_date(), + style={ + "width": "100%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_support_stats_current_month(), + style={ + "width": "100%", + "height": "500px", + "display": "inline-block", + }, + ), + html.H2("Sentry Quota"), + dcc.Graph( + figure=figure_service.get_sentry_transactions_usage(), + style={ + "width": "100%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_sentry_errors_usage(), + style={ + "width": "50%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_sentry_replays_usage(), + style={ + "width": "50%", + "height": "500px", + "display": "inline-block", + }, + ), + html.H2("Github Actions Quota"), + dcc.Graph( + figure=figure_service.get_github_actions_quota_usage_cumulative()[ + 0 + ], + style={ + "width": "100%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_github_actions_quota_usage_cumulative()[ + 1 + ], + style={ + "width": "100%", + "height": "500px", + "display": "inline-block", + }, + ), + html.H1("🙈 Stub Data 🙈"), + dcc.Graph( + figure=figure_service.get_stubbed_number_of_repositories_with_standards_label_dashboard(), + style={ + "width": "33%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_stubbed_number_of_repositories_archived_by_automation(), + style={ + "width": "33%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_stubbed_sentry_transactions_used(), + style={ + "width": "33%", + "height": "500px", + "display": "inline-block", + }, + ), + dcc.Graph( + figure=figure_service.get_support_stats(), + style={ + "width": "100%", + "height": "500px", + "display": "inline-block", + }, + ), + ], + style={"padding": "0px", "margin": "0px", "background-color": "black"}, + ) + + return dashboard diff --git a/app/assets/main.css b/app/assets/main.css new file mode 100644 index 0000000..a4a949d --- /dev/null +++ b/app/assets/main.css @@ -0,0 +1,11 @@ + +body { + margin: 0px; + font-family: monospace; + color: white; +} + +h1 { + padding-top: 20px; + margin-top: 0px; +} \ No newline at end of file diff --git a/app/config/__init__.py b/app/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config/app_config.py b/app/config/app_config.py new file mode 100644 index 0000000..36e7740 --- /dev/null +++ b/app/config/app_config.py @@ -0,0 +1,43 @@ +import os +from types import SimpleNamespace + + +def __get_env_var(name: str) -> str | None: + return os.getenv(name) + + +def __get_env_var_as_boolean(name: str, default: bool) -> bool | None: + value = __get_env_var(name) + + if value is None: + return default + + if value.lower() == "true": + return True + + if value.lower() == "false": + return False + + return default + + +app_config = SimpleNamespace( + api_key=__get_env_var("API_KEY"), + auth_enabled=__get_env_var_as_boolean("AUTH_ENABLED", True), + auth0=SimpleNamespace( + domain=__get_env_var("AUTH0_DOMAIN"), + client_id=__get_env_var("AUTH0_CLIENT_ID"), + client_secret=__get_env_var("AUTH0_CLIENT_SECRET"), + ), + flask=SimpleNamespace( + app_secret_key=__get_env_var("APP_SECRET_KEY"), + ), + logging_level=__get_env_var("LOGGING_LEVEL"), + postgres=SimpleNamespace( + user=__get_env_var("POSTGRES_USER"), + password=__get_env_var("POSTGRES_PASSWORD"), + db=__get_env_var("POSTGRES_DB"), + host=__get_env_var("POSTGRES_HOST"), + port=__get_env_var("POSTGRES_PORT"), + ), +) diff --git a/app/config/logging_config.py b/app/config/logging_config.py new file mode 100644 index 0000000..bf349be --- /dev/null +++ b/app/config/logging_config.py @@ -0,0 +1,10 @@ +import logging + + +def configure_logging(logging_level: str) -> None: + logging.basicConfig( + format="{asctime:s} | {levelname:>8s} | {filename:s}:{lineno:d} | {message:s}", + datefmt="%Y-%m-%dT%H:%M:%S", + style="{", + level=logging_level.upper() if logging_level else "INFO", + ) diff --git a/app/config/routes_config.py b/app/config/routes_config.py new file mode 100644 index 0000000..2bb5576 --- /dev/null +++ b/app/config/routes_config.py @@ -0,0 +1,9 @@ +from flask import Flask + +from app.routes.api_route import api_route +from app.routes.index_route import index_route + + +def configure_routes(app: Flask) -> None: + app.register_blueprint(api_route, url_prefix="/api") + app.register_blueprint(index_route, url_prefix="/") diff --git a/app/routes/api_route.py b/app/routes/api_route.py new file mode 100644 index 0000000..ede64b6 --- /dev/null +++ b/app/routes/api_route.py @@ -0,0 +1,28 @@ +import logging +from typing import Callable + +from flask import Blueprint, current_app, request + +from app.config.app_config import app_config + +logger = logging.getLogger(__name__) + +api_route = Blueprint("api_routes", __name__) + + +def requires_api_key(func: Callable): + def decorator(*args, **kwargs): + if "X-API-KEY" not in request.headers or request.headers.get("X-API-KEY") != app_config.api_key: + return "", 403 + return func(*args, **kwargs) + + return decorator + + +@api_route.route("/indicator/add", methods=["POST"]) +@requires_api_key +def add_indicator(): + indicator = request.get_json().get("indicator") + count = request.get_json().get("count") + current_app.database_service.add_indicator(indicator, count) + return ("", 204) diff --git a/app/routes/index_route.py b/app/routes/index_route.py new file mode 100644 index 0000000..de26752 --- /dev/null +++ b/app/routes/index_route.py @@ -0,0 +1,11 @@ +import logging + +from flask import Blueprint, redirect + +logger = logging.getLogger(__name__) + +index_route = Blueprint("index_routes", __name__) + +@index_route.route("/") +def add_indicator(): + return redirect("/dashboard/", 302) diff --git a/app/run.py b/app/run.py new file mode 100644 index 0000000..08cbbad --- /dev/null +++ b/app/run.py @@ -0,0 +1,17 @@ +from app.app import create_app + + +# Gunicorn entry point - used in production and referenced in the`Dockerfile` +def app(): + return create_app() + + +# Flask entry point - used for local development +def run_app(): + app = create_app() + app.run(port=4567) + return app + + +if __name__ == "__main__": + run_app() diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/database_service.py b/app/services/database_service.py new file mode 100644 index 0000000..fb0230f --- /dev/null +++ b/app/services/database_service.py @@ -0,0 +1,61 @@ +import datetime +import logging +from typing import Any + +import psycopg2 + +from app.config.app_config import app_config + +logger = logging.getLogger(__name__) + + +class DatabaseService: + def __execute_query(self, sql: str, values=None): + with psycopg2.connect( + dbname=app_config.postgres.db, + user=app_config.postgres.user, + password=app_config.postgres.password, + host=app_config.postgres.host, + port=app_config.postgres.port, + ) as conn: + logging.info("Executing query...") + cur = conn.cursor() + cur.execute(sql, values) + data = None + if cur.description is not None: + try: + data = cur.fetchall() + except Exception as e: + logging.error(e) + + conn.commit() + return data + + def get_indicator(self, indicator: str) -> list[tuple[Any, Any]]: + return self.__execute_query( + sql="SELECT timestamp, count FROM indicators WHERE indicator = %s;", + values=[indicator], + ) + + def create_indicators_table(self) -> None: + self.__execute_query( + sql=""" + CREATE TABLE IF NOT EXISTS indicators ( + id SERIAL PRIMARY KEY, + indicator varchar, + timestamp timestamp, + count integer + ); + """ + ) + + def clean_stubbed_indicators_table(self) -> None: + self.__execute_query( + sql="DELETE FROM indicators WHERE indicator LIKE 'STUBBED%'" + ) + + def add_indicator(self, indicator, count) -> None: + self.__execute_query( + "INSERT INTO indicators (indicator,timestamp, count) VALUES (%s, %s, %s);", + values=[indicator, datetime.datetime.now(), count], + ) diff --git a/app/services/figure_service.py b/app/services/figure_service.py new file mode 100644 index 0000000..286809d --- /dev/null +++ b/app/services/figure_service.py @@ -0,0 +1,334 @@ +import logging + +import pandas as pd +from datetime import date, timedelta +import plotly.express as px + +logger = logging.getLogger(__name__) + + +class FigureService: + def __init__(self, database_service) -> None: + self.database_service = database_service + + def get_number_of_repositories_with_standards_label_dashboard(self): + number_of_repos_with_standards_label_df = pd.DataFrame( + self.database_service.get_indicator("REPOSITORIES_WITH_STANDARDS_LABEL"), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_number_of_repositories_with_standards_label = px.line( + number_of_repos_with_standards_label_df, + x="timestamp", + y="count", + title="🏷️ Number of Repositories With Standards Label", + markers=True, + template="plotly_dark", + ) + fig_number_of_repositories_with_standards_label.add_hline(y=0) + + return fig_number_of_repositories_with_standards_label + + def get_stubbed_number_of_repositories_with_standards_label_dashboard(self): + number_of_repos_with_standards_label_df = pd.DataFrame( + self.database_service.get_indicator( + "STUBBED_REPOSITORIES_WITH_STANDARDS_LABEL" + ), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_stubbed_number_of_repositories_with_standards_label = px.line( + number_of_repos_with_standards_label_df, + x="timestamp", + y="count", + title="🏷️ Number of Repositories With Standards Label", + markers=True, + template="plotly_dark", + ) + fig_stubbed_number_of_repositories_with_standards_label.add_hline(y=0) + + return fig_stubbed_number_of_repositories_with_standards_label + + def get_stubbed_number_of_repositories_archived_by_automation(self): + number_of_repositories_archived_by_automation = pd.DataFrame( + self.database_service.get_indicator( + "STUBBED_REPOSITORIES_ARCHIVED_BY_AUTOMATION" + ), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_stubbed_number_of_repositories_archived_by_automation = px.line( + number_of_repositories_archived_by_automation, + x="timestamp", + y="count", + title="👴 Number of Repositories Archived By Automation", + markers=True, + template="plotly_dark", + ) + fig_stubbed_number_of_repositories_archived_by_automation.add_hline(y=0) + + return fig_stubbed_number_of_repositories_archived_by_automation + + def get_stubbed_sentry_transactions_used(self): + sentry_transaction_quota_consumed = pd.DataFrame( + self.database_service.get_indicator( + "STUBBED_SENTRY_DAILY_TRANSACTION_USAGE" + ), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_stubbed_sentry_transactions_used = px.line( + sentry_transaction_quota_consumed, + x="timestamp", + y="count", + title="👀 Sentry Transactions Used", + markers=True, + template="plotly_dark", + ) + fig_stubbed_sentry_transactions_used.add_hline( + y=967741, annotation_text="Max Daily Usage" + ) + fig_stubbed_sentry_transactions_used.add_hrect( + y0=(967741 * 0.8), + y1=967741, + line_width=0, + fillcolor="red", + opacity=0.2, + annotation_text="Alert Threshold", + ) + + return fig_stubbed_sentry_transactions_used + + def get_support_stats(self): + support_stats_csv = pd.read_csv("data/support-stats.csv") + support_stats_csv_pivoted = pd.melt( + support_stats_csv, + value_vars=[ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ], + id_vars=["Request Type", "Total"], + value_name="Count", + var_name="Month", + ignore_index=True, + ) + + fig_support_stats = px.line( + support_stats_csv_pivoted, + x="Month", + y="Count", + color="Request Type", + title="🏋️ Support Stats", + markers=True, + template="plotly_dark", + ) + + return fig_support_stats + + def get_support_stats_year_to_date(self): + support_requests_all = pd.read_csv("production/support_request_stats.csv") + support_requests_all = ( + support_requests_all.groupby(by=["Date", "Type"]) + .size() + .reset_index(name="Count") + ) + + fig_support_stats_year_to_date = px.line( + support_requests_all, + x="Date", + y="Count", + color="Type", + title="Support Requests by Type - Year to Date", + template="plotly_dark", + ) + + return fig_support_stats_year_to_date + + def get_support_stats_current_month(self): + month = date.today().month + support_requests_current_month = pd.read_csv( + "production/support_request_stats.csv" + ) + support_requests_current_month["Date"] = pd.to_datetime( + support_requests_current_month["Date"], format="%Y-%m-%d" + ) + support_requests_current_month = support_requests_current_month.loc[ + support_requests_current_month["Date"].dt.month == month + ] + support_requests_current_month["Total"] = ( + support_requests_current_month.groupby("Date")["Type"].transform("size") + ) + + fig_support_stats_current_month = px.bar( + support_requests_current_month, + x="Date", + y="Type", + color="Type", + title="Support Requests by Type - Current Month", + template="plotly_dark", + ) + + return fig_support_stats_current_month + + def get_github_actions_quota_usage_cumulative(self): + + github_actions_quota_usage_cumulative = pd.DataFrame( + self.database_service.get_indicator( + "ENTERPRISE_GITHUB_ACTIONS_QUOTA_USAGE" + ), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_github_actions_quota_usage_cumulative = px.line( + github_actions_quota_usage_cumulative, + x="timestamp", + y="count", + title="Cumulative Github Actions Usage", + markers=True, + template="plotly_dark", + ) + fig_github_actions_quota_usage_cumulative.add_hline( + y=50000, + line=dict(color="red", dash="dash"), + annotation_text="Minutes allowance", + annotation_position="top right" + ) + fig_github_actions_quota_usage_cumulative.update_layout( + yaxis_title="Min used" + ) + + # Add quota reset lines + start_date = github_actions_quota_usage_cumulative['timestamp'].min().date() + end_date = github_actions_quota_usage_cumulative['timestamp'].max().date() + max_y = github_actions_quota_usage_cumulative['count'].max() + + start_days_quota = pd.date_range(start=start_date, end=end_date, freq='MS') + + for day in start_days_quota: + fig_github_actions_quota_usage_cumulative.add_vline( + x=day, + line_dash="dash", + line_color="white" + ) + fig_github_actions_quota_usage_cumulative.add_annotation( + x=day, y=max_y, text="Quota Reset", ax=-25) + + # Daily gha consumption graph + github_actions_quota_usage_daily = github_actions_quota_usage_cumulative.copy() + github_actions_quota_usage_daily['Month'] = github_actions_quota_usage_daily['timestamp'].dt.to_period('M') + github_actions_quota_usage_daily['Daily_minutes'] = github_actions_quota_usage_daily.groupby('Month')['count'].diff() + github_actions_quota_usage_daily['Date'] = (github_actions_quota_usage_daily['timestamp'] - timedelta(days=1)).dt.date + + github_actions_quota_usage_daily = github_actions_quota_usage_daily.dropna() + + fig_github_actions_quota_usage_daily = px.bar( + github_actions_quota_usage_daily, + x="Date", + y="Daily_minutes", + title="Daily Github Actions Usage", + template="plotly_dark", + ) + fig_github_actions_quota_usage_daily.update_layout( + yaxis_title="Min used" + ) + + fig_github_actions_quota_usage_daily.add_hline( + y=github_actions_quota_usage_daily['Daily_minutes'].median(), + line=dict(color="red", dash="dash"), # Custom line style + annotation_text="Median", + annotation_position="top right" + ) + + return fig_github_actions_quota_usage_cumulative, fig_github_actions_quota_usage_daily + + def get_sentry_transactions_usage(self): + sentry_transaction_quota_consumed = pd.DataFrame( + self.database_service.get_indicator( + "SENTRY_TRANSACTIONS_USED_OVER_PAST_DAY" + ), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_stubbed_sentry_transactions_used = px.line( + sentry_transaction_quota_consumed, + x="timestamp", + y="count", + title="Sentry Transactions Used", + markers=True, + template="plotly_dark", + ) + fig_stubbed_sentry_transactions_used.add_hline( + y=967741, annotation_text="Max Daily Usage" + ) + fig_stubbed_sentry_transactions_used.add_hrect( + y0=(967741 * 0.8), + y1=967741, + line_width=0, + fillcolor="red", + opacity=0.2, + annotation_text="Alert Threshold", + ) + + return fig_stubbed_sentry_transactions_used + + def get_sentry_errors_usage(self): + sentry_errors_quota_consumed = pd.DataFrame( + self.database_service.get_indicator("SENTRY_ERRORS_USED_OVER_PAST_DAY"), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_sentry_erros_used = px.line( + sentry_errors_quota_consumed, + x="timestamp", + y="count", + title="Errors Used", + markers=True, + template="plotly_dark", + ) + fig_sentry_erros_used.add_hline(y=129032, annotation_text="Max Daily Usage") + fig_sentry_erros_used.add_hrect( + y0=(129032 * 0.8), + y1=129032, + line_width=0, + fillcolor="red", + opacity=0.2, + annotation_text="Alert Threshold", + ) + + return fig_sentry_erros_used + + def get_sentry_replays_usage(self): + sentry_replays_quota_consumed = pd.DataFrame( + self.database_service.get_indicator("SENTRY_REPLAYS_USED_OVER_PAST_DAY"), + columns=["timestamp", "count"], + ).sort_values(by="timestamp", ascending=True) + + fig_sentry_replays_used = px.line( + sentry_replays_quota_consumed, + x="timestamp", + y="count", + title="Replays Used", + markers=True, + template="plotly_dark", + ) + fig_sentry_replays_used.add_hline(y=25806, annotation_text="Max Daily Usage") + fig_sentry_replays_used.add_hrect( + y0=(25806 * 0.8), + y1=25806, + line_width=0, + fillcolor="red", + opacity=0.2, + annotation_text="Alert Threshold", + ) + + return fig_sentry_replays_used diff --git a/docker-compose-grafana.yaml b/docker-compose-grafana.yaml new file mode 100644 index 0000000..f34212f --- /dev/null +++ b/docker-compose-grafana.yaml @@ -0,0 +1,46 @@ +version: "3.9" +services: + app: + build: . + image: "kubera" + container_name: "kubera" + depends_on: + - postgres + environment: + POSTGRES_PASSWORD: admin + POSTGRES_USER: admin + POSTGRES_DB: admin + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + APP_SECRET_KEY: dev + FLASK_DEBUG: true + ports: + - "4567:4567" + networks: + - kubera + + postgres: + image: postgres:14-alpine + container_name: "postgres" + ports: + - 5432:5432 + environment: + - POSTGRES_PASSWORD=admin + - POSTGRES_USER=admin + - POSTGRES_DB=admin + networks: + - kubera + + grafana: + build: + dockerfile: ./grafana/Dockerfile + context: ./ + container_name: grafana + restart: unless-stopped + ports: + - "3000:3000" + networks: + - kubera + +networks: + kubera: diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..8110554 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,44 @@ +version: "3.9" +services: + app: + build: . + image: "kubera" + container_name: "kubera" + environment: + AUTH_ENABLED: false + POSTGRES_PASSWORD: admin + POSTGRES_USER: admin + POSTGRES_DB: admin + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + APP_SECRET_KEY: dev + FLASK_DEBUG: true + API_KEY: test + ports: + - "4567:4567" + depends_on: + postgres: + condition: service_healthy + restart: true + networks: + - kubera + + postgres: + image: postgres:14-alpine + container_name: "postgres" + ports: + - 5432:5432 + environment: + - POSTGRES_PASSWORD=admin + - POSTGRES_USER=admin + - POSTGRES_DB=admin + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U admin" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - kubera + +networks: + kubera: diff --git a/grafana/Dockerfile b/grafana/Dockerfile new file mode 100644 index 0000000..15d4e91 --- /dev/null +++ b/grafana/Dockerfile @@ -0,0 +1,20 @@ +FROM grafana/grafana:10.4.2-ubuntu + +# Disable Login form or not +ENV GF_AUTH_DISABLE_LOGIN_FORM "true" +# Allow anonymous authentication or not +ENV GF_AUTH_ANONYMOUS_ENABLED "true" +# Role of anonymous user +ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" +# Install plugins here our in your own config file +ENV GF_INSTALL_PLUGINS="marcusolsson-csv-datasource" + +# Copy CSV Data Into Grafana +COPY ./example-data /data/ + +# Add provisioning +ADD ./grafana/provisioning /etc/grafana/provisioning +# Add configuration file +ADD ./grafana/grafana.ini /etc/grafana/grafana.ini +# Add dashboard json files +ADD ./grafana/dashboards /etc/grafana/dashboards diff --git a/grafana/dashboards/plotly-dashboard-copy.json b/grafana/dashboards/plotly-dashboard-copy.json new file mode 100644 index 0000000..4eca658 --- /dev/null +++ b/grafana/dashboards/plotly-dashboard-copy.json @@ -0,0 +1,687 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 6, + "panels": [], + "title": "From Postgres", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P44368ADAD746BC27" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 17, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "displayName": "Repositories With Standards Label", + "fieldMinMax": false, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P44368ADAD746BC27" + }, + "editorMode": "builder", + "format": "table", + "rawSql": "SELECT \"timestamp\", count FROM indicators WHERE indicator = 'REPOSITORIES_WITH_STANDARDS_LABEL' LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [ + { + "name": "\"timestamp\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "count", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "b9abab9b-4567-489a-bcde-f18f350054e6", + "properties": { + "field": "indicator", + "fieldSrc": "field", + "operator": "equal", + "value": [ + "REPOSITORIES_WITH_STANDARDS_LABEL" + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "899a98ba-0123-4456-b89a-b18f34fdcf7d", + "type": "group" + }, + "whereString": "indicator = 'REPOSITORIES_WITH_STANDARDS_LABEL'" + }, + "table": "indicators" + } + ], + "title": "🏷️ Number of Repositories With Standards Label", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P44368ADAD746BC27" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 17, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "displayName": "Archived Repositories", + "fieldMinMax": false, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P44368ADAD746BC27" + }, + "editorMode": "builder", + "format": "table", + "rawSql": "SELECT \"timestamp\", count FROM indicators WHERE indicator = 'REPOSITORIES_ARCHIVED_BY_AUTOMATION' LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [ + { + "name": "\"timestamp\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "count", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "b9abab9b-4567-489a-bcde-f18f350054e6", + "properties": { + "field": "indicator", + "fieldSrc": "field", + "operator": "equal", + "value": [ + "REPOSITORIES_ARCHIVED_BY_AUTOMATION" + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "899a98ba-0123-4456-b89a-b18f34fdcf7d", + "type": "group" + }, + "whereString": "indicator = 'REPOSITORIES_ARCHIVED_BY_AUTOMATION'" + }, + "table": "indicators" + } + ], + "title": "👴 Number of Repositories Archived by Automation", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P44368ADAD746BC27" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 17, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "displayName": "Transactions", + "fieldMinMax": false, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 774192 + }, + { + "color": "red", + "value": 967741 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P44368ADAD746BC27" + }, + "editorMode": "builder", + "format": "table", + "rawSql": "SELECT \"timestamp\", count FROM indicators WHERE indicator = 'SENTRY_DAILY_TRANSACTION_USAGE' LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [ + { + "name": "\"timestamp\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "count", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "b9abab9b-4567-489a-bcde-f18f350054e6", + "properties": { + "field": "indicator", + "fieldSrc": "field", + "operator": "equal", + "value": [ + "SENTRY_DAILY_TRANSACTION_USAGE" + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "899a98ba-0123-4456-b89a-b18f34fdcf7d", + "type": "group" + }, + "whereString": "indicator = 'SENTRY_DAILY_TRANSACTION_USAGE'" + }, + "table": "indicators" + } + ], + "title": "👀 Sentry Transactions Usage - Provisioned", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 5, + "panels": [], + "title": "From CSV", + "type": "row" + }, + { + "datasource": { + "type": "marcusolsson-csv-datasource", + "uid": "1257c93b-f998-438c-a784-7e90fb94fb3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 1000 + }, + { + "color": "red", + "value": 1300 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "marcusolsson-csv-datasource", + "uid": "1257c93b-f998-438c-a784-7e90fb94fb3" + }, + "decimalSeparator": ".", + "delimiter": ",", + "header": true, + "ignoreUnknown": true, + "refId": "A", + "schema": [ + { + "name": "Date", + "type": "time" + }, + { + "name": "Product", + "type": "string" + }, + { + "name": "Quantity", + "type": "number" + } + ], + "skipRows": 0 + } + ], + "title": "💥 GitHub Actions Usage Quota", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "Actions" + } + }, + "fieldName": "Product" + } + ], + "match": "all", + "type": "include" + } + }, + { + "id": "groupBy", + "options": { + "fields": { + "Date": { + "aggregations": [], + "operation": "groupby" + }, + "Quantity": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + } + } + } + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2023-10-31T05:24:34.422Z", + "to": "2024-05-02T01:53:51.843Z" + }, + "timepicker": {}, + "timezone": "", + "title": "🙈 Trying to Copy The Dash/Plotly Dashboard at http://localhost:4567 - Provisioned", + "uid": "cdkesxg2h6cxsd", + "version": 6, + "weekStart": "" +} \ No newline at end of file diff --git a/grafana/grafana.ini b/grafana/grafana.ini new file mode 100644 index 0000000..71349c2 --- /dev/null +++ b/grafana/grafana.ini @@ -0,0 +1,26 @@ +[paths] +provisioning = /etc/grafana/provisioning + +[server] +enable_gzip = true +# To add HTTPS support: +#protocol = https +#;http_addr = +#http_port = 3000 +#domain = localhost +#enforce_domain = false +#root_url = https://localhost:3000 +#router_logging = false +#static_root_path = public +#cert_file = /etc/certs/cert.pem +#cert_key = /etc/certs/cert-key.pem + +[security] +# If you want to embed grafana into an iframe for example +allow_embedding = true + +[users] +default_theme = dark + +[plugin.marcusolsson-csv-datasource] +allow_local_mode = true \ No newline at end of file diff --git a/grafana/provisioning/dashboards/dashboard.yml b/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000..c45c30e --- /dev/null +++ b/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,25 @@ +# config file version +apiVersion: 1 + +providers: +# an unique provider name +- name: My Dashboard + # org id. will default to orgId 1 if not specified + org_id: 1 + # name of the dashboard folder. Required + folder: '' + # provider type. Required + type: 'file' + # disable dashboard deletion + disableDeletion: false + # enable dashboard editing + editable: true + # how often Grafana will scan for changed dashboards + updateIntervalSeconds: 5 + # allow updating provisioned dashboards from the UI + allowUiUpdates: true + options: + # path to dashboard files on disk. Required + path: /etc/grafana/dashboards + # use folder names from filesystem to create folders in Grafana + foldersFromFilesStructure: true \ No newline at end of file diff --git a/grafana/provisioning/datasources/datasource.yml b/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 0000000..8ca7094 --- /dev/null +++ b/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,40 @@ +apiVersion: 1 + +datasources: + - name: Postgres + type: postgres + url: postgres:5432 + user: admin + secureJsonData: + password: "admin" + jsonData: + database: admin + sslmode: "disable" # disable/require/verify-ca/verify-full + maxOpenConns: 100 # Grafana v5.4+ + maxIdleConns: 100 # Grafana v5.4+ + maxIdleConnsAuto: true # Grafana v9.5.1+ + connMaxLifetime: 14400 # Grafana v5.4+ + postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10 + timescaledb: false + - name: Support Stats + uid: 1257c93b-f998-438c-a784-7e90fb94fb36 + url: /data/support-stats.csv + type: marcusolsson-csv-datasource + access: "proxy" + basicAuth: false + isDefault: false + editable: true + version: 1 + jsonData: + storage: local + - name: GitHub Quota Usage + uid: 1257c93b-f998-438c-a784-7e90fb94fb3 + url: /data/github_actions_private_and_internal.csv + type: marcusolsson-csv-datasource + access: "proxy" + basicAuth: false + isDefault: false + editable: true + version: 1 + jsonData: + storage: local diff --git a/helm/kubera/.helmignore b/helm/kubera/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/kubera/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/kubera/Chart.yaml b/helm/kubera/Chart.yaml new file mode 100644 index 0000000..09ea0eb --- /dev/null +++ b/helm/kubera/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: kubera +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/kubera/templates/_helpers.tpl b/helm/kubera/templates/_helpers.tpl new file mode 100644 index 0000000..301c68a --- /dev/null +++ b/helm/kubera/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "kubera.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "kubera.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "kubera.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "kubera.labels" -}} +helm.sh/chart: {{ include "kubera.chart" . }} +{{ include "kubera.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "kubera.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kubera.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/helm/kubera/templates/deployment.yaml b/helm/kubera/templates/deployment.yaml new file mode 100644 index 0000000..1242fef --- /dev/null +++ b/helm/kubera/templates/deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "kubera.fullname" . }} + labels: {{- include "kubera.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.app.deployment.replicaCount }} + selector: + matchLabels: {{- include "kubera.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: {{- include "kubera.selectorLabels" . | nindent 8 }} + spec: + # serviceAccountName created by the Cloud Platform environment + serviceAccountName: cd-serviceaccount + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.app.deployment.image.repository }}:{{ .Values.app.deployment.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: IfNotPresent + env: + - name: API_KEY + value: {{ .Values.app.deployment.env.API_KEY | quote }} + - name: AUTH0_DOMAIN + value: {{ .Values.app.deployment.env.AUTH0_DOMAIN | quote}} + - name: AUTH0_CLIENT_ID + value: {{ .Values.app.deployment.env.AUTH0_CLIENT_ID | quote}} + - name: AUTH0_CLIENT_SECRET + value: {{ .Values.app.deployment.env.AUTH0_CLIENT_SECRET | quote}} + - name: APP_SECRET_KEY + value: {{ .Values.app.deployment.env.APP_SECRET_KEY | quote}} + - name: POSTGRES_USER + value: {{ .Values.app.deployment.env.POSTGRES_USER | quote }} + - name: POSTGRES_PASSWORD + value: + {{ .Values.app.deployment.env.POSTGRES_PASSWORD | quote }} + - name: POSTGRES_DB + value: {{ .Values.app.deployment.env.POSTGRES_DB | quote }} + - name: POSTGRES_HOST + value: {{ .Values.app.deployment.env.POSTGRES_HOST | quote }} + - name: POSTGRES_PORT + value: {{ .Values.app.deployment.env.POSTGRES_PORT | quote }} + + ports: + - name: http + containerPort: 80 diff --git a/helm/kubera/templates/ingress.yaml b/helm/kubera/templates/ingress.yaml new file mode 100644 index 0000000..67f19cd --- /dev/null +++ b/helm/kubera/templates/ingress.yaml @@ -0,0 +1,29 @@ +{{- $fullName := include "kubera.fullname" . -}} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "kubera.labels" . | nindent 4 }} + annotations: + external-dns.alpha.kubernetes.io/set-identifier: {{ $fullName }}-{{ $fullName }}-green + external-dns.alpha.kubernetes.io/aws-weight: "100" + cloud-platform.justice.gov.uk/ignore-external-dns-weight: "true" +spec: + ingressClassName: "default" + tls: + - hosts: + - {{ .Values.app.ingress.host }} + rules: + - host: {{ .Values.app.ingress.host }} + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: {{ $fullName }} + port: + number: 80 + diff --git a/helm/kubera/templates/service.yaml b/helm/kubera/templates/service.yaml new file mode 100644 index 0000000..c84b337 --- /dev/null +++ b/helm/kubera/templates/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kubera.fullname" . }} + labels: {{- include "kubera.labels" . | nindent 4 }} +spec: + ports: + - port: 80 + targetPort: 4567 + name: https + selector: {{- include "kubera.selectorLabels" . | nindent 4 }} diff --git a/helm/kubera/values-prod.yaml b/helm/kubera/values-prod.yaml new file mode 100644 index 0000000..72b4e46 --- /dev/null +++ b/helm/kubera/values-prod.yaml @@ -0,0 +1,8 @@ +app: + ingress: + host: "" + + deployment: + replicaCount: 1 + env: + LOGGING_LEVEL: INFO