diff --git a/.env.example b/.env.example
index d495d97..d10ace2 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,8 @@
-FLASK_APP=wsgi.py
-FLASK_ENV=development
+FLASK_ENV=production
+FLASK_DEBUG=False
SECRET_KEY=randomstringofcharacters
+
LESS_BIN=/usr/local/bin/lessc
+COMPRESSOR_DEBUG=False
ASSETS_DEBUG=False
-LESS_RUN_IN_DEBUG=False
-COMPRESSOR_DEBUG=True
\ No newline at end of file
+LESS_RUN_IN_DEBUG=False
\ No newline at end of file
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..00e2b94
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,4 @@
+[flake8]
+select = E9,F63,F7,F82
+exclude = .git,.github,__pycache__,.pytest_cache,.venv,logs,creds
+max-line-length = 120
\ No newline at end of file
diff --git a/.github/dash@2x.jpg b/.github/dash@2x.jpg
new file mode 100644
index 0000000..e301c55
Binary files /dev/null and b/.github/dash@2x.jpg differ
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 38bb39c..620cca4 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -43,7 +43,7 @@ jobs:
# Run Linter against code base #
################################
- name: Lint Code Base
- uses: docker://github/super-linter:v2.2.0
+ uses: docker://github/super-linter:v2.2.2@sha256:440bc28f423a1c497b2ef72a18b180d92708c0943355e53189cc98605d808bf8
env:
VALIDATE_ALL_CODEBASE: false
VALIDATE_ANSIBLE: false
diff --git a/Makefile b/Makefile
index 2275a41..32c0585 100644
--- a/Makefile
+++ b/Makefile
@@ -1,42 +1,93 @@
-SRCPATH := $(CURDIR)
-ENTRYPOINT := $(shell find $(SRCPATH) -name '*.ini')
-PROJECTNAME := $(shell basename "$PWD")
+PROJECT_NAME := $(shell basename $CURDIR)
+VIRTUAL_ENV := $(CURDIR)/.venv
+LOCAL_PYTHON := $(VIRTUAL_ENV)/bin/python3
define HELP
-Manage $(PROJECTNAME).
+Manage $(PROJECT_NAME). Usage:
-Usage:
+make run - Run $(PROJECT_NAME) locally.
+make install - Create local virtualenv & install dependencies.
+make deploy - Set up project & run locally.
+make update - Update dependencies via Poetry and output resulting `requirements.txt`.
+make format - Run Python code formatter & sort dependencies.
+make lint - Check code formatting with flake8.
+make clean - Remove extraneous compiled files, caches, logs, etc.
-make run - Run uWSGI server for $(PROJECTNAME).
-make restart - Purge cache & reinstall modules.
-make update - Update npm production dependencies.
-make clean - Remove cached files.
endef
export HELP
-.PHONY: run restart update help
+
+.PHONY: run install deploy update format lint clean help
all help:
@echo "$$HELP"
+env: $(VIRTUAL_ENV)
+
+$(VIRTUAL_ENV):
+ if [ ! -d $(VIRTUAL_ENV) ]; then \
+ echo "Creating Python virtual env in \`${VIRTUAL_ENV}\`"; \
+ python3 -m venv $(VIRTUAL_ENV); \
+ fi
+
.PHONY: run
-run:
- nohup uwsgi $(ENTRYPOINT) &
+run: env
+ $(LOCAL_PYTHON) -m main
+
+.PHONY: install
+install: env
+ $(LOCAL_PYTHON) -m pip install --upgrade pip setuptools wheel && \
+ $(LOCAL_PYTHON) -m pip install -r requirements.txt && \
+ npm i -g less && \
+ echo Installed dependencies in \`${VIRTUAL_ENV}\`;
-.PHONY: restart
-restart:
- pkill -9 -f $(shell uwsgi $(ENTRYPOINT))
- nohup uwsgi $(ENTRYPOINT) &
+.PHONY: deploy
+deploy:
+ make install && \
+ make run
+
+.PHONY: test
+test: env
+ $(LOCAL_PYTHON) -m \
+ coverage run -m pytest -vv \
+ --disable-pytest-warnings && \
+ coverage html --title='Coverage Report' -d .reports && \
+ open .reports/index.html
.PHONY: update
-update:
- git pull origin master
- pkill -9 -f $(shell uwsgi $(ENTRYPOINT))
- poetry shell
- poetry update
- nohup uwsgi $(ENTRYPOINT) &
+update: env
+ $(LOCAL_PYTHON) -m pip install --upgrade pip setuptools wheel && \
+ poetry update && \
+ poetry export -f requirements.txt --output requirements.txt --without-hashes && \
+ echo Installed dependencies in \`${VIRTUAL_ENV}\`;
+
+.PHONY: format
+format: env
+ $(LOCAL_PYTHON) -m isort --multi-line=3 . && \
+ $(LOCAL_PYTHON) -m black .
+
+.PHONY: lint
+lint: env
+ $(LOCAL_PYTHON) -m flake8 . --count \
+ --select=E9,F63,F7,F82 \
+ --exclude .git,.github,__pycache__,.pytest_cache,.venv,logs,creds,.venv,docs,logs,.reports \
+ --show-source \
+ --statistics
.PHONY: clean
clean:
- find . -name '*.pyc' -delete
- find . -name '__pycache__' -delete
+ find . -name '.coverage' -delete && \
+ find . -name '*.pyc' -delete \
+ find . -name '__pycache__' -delete \
+ find . -name 'poetry.lock' -delete \
+ find . -name '*.log' -delete \
+ find . -name '.DS_Store' -delete \
+ find . -wholename '**/*.pyc' -delete && \
+ find . -wholename '**/*.html' -delete && \
+ find . -type d -wholename '__pycache__' -exec rm -rf {} + && \
+ find . -type d -wholename '.venv' -exec rm -rf {} + && \
+ find . -type d -wholename '.pytest_cache' -exec rm -rf {} + && \
+ find . -type d -wholename '**/.pytest_cache' -exec rm -rf {} + && \
+ find . -type d -wholename '**/*.log' -exec rm -rf {} + && \
+ find . -type d -wholename './.reports/*' -exec rm -rf {} + && \
+ find . -type d -wholename '**/.webassets-cache' -exec rm -rf {} +
\ No newline at end of file
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 4d73d1c..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,22 +0,0 @@
-[[source]]
-name = "pypi"
-url = "https://pypi.org/simple"
-verify_ssl = true
-
-[dev-packages]
-lesscpy = "*"
-cssmin = "*"
-
-[packages]
-Flask = "*"
-Flask_assets = "*"
-Dash = "*"
-Dash_Table = "*"
-Dash_core_components = "*"
-Dash_html_components = "*"
-Dash-renderer = "*"
-Pandas = "*"
-python-dotenv = "*"
-
-[requires]
-python_version = "3.8"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index f99e04f..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,320 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "d2517b51ae1b24f6a3be8404c5ae5fdda4b01ac6a6c9f0e7cc35047b60b130e4"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.8"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "brotli": {
- "hashes": [
- "sha256:0538dc1744fd17c314d2adc409ea7d1b779783b89fd95bcfb0c2acc93a6ea5a7",
- "sha256:0970a47f471782912d7705160b2b0a9306e68e6fadf9cffcaeb42d8f0951e26c",
- "sha256:113f51658e6fe548dce4b3749f6ef6c24de4184ba9c10a909cbee4261c2a5da0",
- "sha256:1e1aa9c4d1558889f42749c8baf846007953bfd32c8209230cf1cd1f5ef33495",
- "sha256:2f2f4f78f29ac4a45d15b3d9fc3fd9705e0ad313a44b129f6e1d0c6916bad0e2",
- "sha256:3269f6de1dd150fd0cce1c158b61ff5ac06d627fd3ae9c6ea03aed26fbbff7ea",
- "sha256:3f4a1f6240916c7984c7f2542786710f622992508dafee0b1714e6d340fb9ffd",
- "sha256:50dd9ad2a2bb12da4e9002a438672d182f98e546e99952de80280a1e1729664f",
- "sha256:5519a4b01b1a4f965083cbfa2ef2b9774c5a5f352341c47b50776ad109423d72",
- "sha256:5eb27722d320370315971c427eb8aa7cc0791f2a458840d357ac653bd0ad3a14",
- "sha256:5f06b4d5b6f58e5b5c220c2f23cad034dc5efa51b01fde2351ced1605bd980e2",
- "sha256:71ceee286ea7ec613f1c36f1c6181864a6ca24ebb55e371276f33d6af8742834",
- "sha256:72848d25a5f9e736db4af4512e0c3feecc094d57d241f8f1ae959115a2c39756",
- "sha256:743001bca75f4a6b4454be3510feca46f9d61a0c782a9bc2bc684bdb245e279e",
- "sha256:7ac98c71a15648fd11bc1f32608b6110e396121280790082e32b9a3109048bc6",
- "sha256:9d1c2dd27a1083fefd05b1b2f8df4a6bc2aaa6c21dd82cd41c8ae5e7c23a87f8",
- "sha256:a13ce9b419fe9f277c63f700efb0e444331509d1881b5610d2ba7e9080606967",
- "sha256:a19ef0952b9d2803df88dff07f45a6c92d5676afb9b8d69cf32232d684036d11",
- "sha256:ad766ca8b8c1419b71a22756b45264f45725c86133dc80a7cbe30b6b78c75620",
- "sha256:ad7963f261988ee0883816b6b9f206f11461c9b3cb5cfbca0c9ab5adc406d395",
- "sha256:af0451e23016631a2f52925a10d738ac4a0f794ac315c30380b22efc0c90cbc6",
- "sha256:c16201060c5a3f8742e3deae759014251ac92f382f82bc2a41dc079ff18c3f24",
- "sha256:c43b202f65891861a9a336984a103de25de235f756de69e32db893156f767013",
- "sha256:c675c6cce4295cb1a692f3de7416aacace7314e064b94bc86e93aceefce7fd3e",
- "sha256:d17cec0b992b1434f5f9df9986563605a4d1b1acd5574c87fc2ac014bcbd3316",
- "sha256:dc91f6129953861a73d9a65c52a8dd682b561a9ebaf65283541645cab6489917",
- "sha256:e2f4cbd1760d2bf2f30e396c2301999aab0191aec031a6a8a04950b2f575a536",
- "sha256:f192e6d3556714105c10486bbd6d045e38a0c04d9da3cef21e0a8dfd8e162df4",
- "sha256:f775b07026af2b1b0b5a8b05e41571cdcf3a315a67df265d60af301656a5425b",
- "sha256:f969ec7f56ba9636679e69ca07fba548312ccaca37412ee823c7f413541ad7e0",
- "sha256:f9dc52cd70907aafb99a773b66b156f2f995c7a0d284397c487c8b71ddbef2f9",
- "sha256:f9ee88bb52352588ceb811d045b5c9bb1dc38927bc150fd156244f60ff3f59f1",
- "sha256:fc7212e36ebeb81aebf7949c92897b622490d7c0e333a479c0395591e7994600"
- ],
- "version": "==1.0.7"
- },
- "click": {
- "hashes": [
- "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
- "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
- ],
- "version": "==7.1.2"
- },
- "dash": {
- "hashes": [
- "sha256:7a55e2f5381bf3323873a9b5e7dbb8112a5c8c5e7fe7e554bda2ed9d3f3e04fc"
- ],
- "index": "pypi",
- "version": "==1.14.0"
- },
- "dash-core-components": {
- "hashes": [
- "sha256:6152346ff2ac8a7fcdb76c8b8acbf3ee4e72f3822cd2a02a9f3a963db66f94a3"
- ],
- "index": "pypi",
- "version": "==1.10.2"
- },
- "dash-html-components": {
- "hashes": [
- "sha256:dafb54ae8ab601fffe50c74d72b32783dec2beea65fd1c7e7dd6a66e20e545ba"
- ],
- "index": "pypi",
- "version": "==1.0.3"
- },
- "dash-renderer": {
- "hashes": [
- "sha256:8e9c0c0c2efb8ea562489c37665417cd608c30bca20425ac4d847420b5bbc128"
- ],
- "index": "pypi",
- "version": "==1.6.0"
- },
- "dash-table": {
- "hashes": [
- "sha256:2f19000051730291100cd3a79b13fa62c478aea7908f2e4323c13b90f09e3320"
- ],
- "index": "pypi",
- "version": "==4.9.0"
- },
- "flask": {
- "hashes": [
- "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
- "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
- ],
- "index": "pypi",
- "version": "==1.1.2"
- },
- "flask-assets": {
- "hashes": [
- "sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2",
- "sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"
- ],
- "index": "pypi",
- "version": "==2.0"
- },
- "flask-compress": {
- "hashes": [
- "sha256:f367b2b46003dd62be34f7fb1379938032656dca56377a9bc90e7188e4289a7c"
- ],
- "version": "==1.5.0"
- },
- "future": {
- "hashes": [
- "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
- ],
- "version": "==0.18.2"
- },
- "itsdangerous": {
- "hashes": [
- "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
- "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
- ],
- "version": "==1.1.0"
- },
- "jinja2": {
- "hashes": [
- "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
- "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
- ],
- "version": "==2.11.2"
- },
- "markupsafe": {
- "hashes": [
- "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
- "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
- "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
- "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
- "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
- "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
- "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
- "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
- "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
- "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
- "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
- "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
- "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
- "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
- "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
- "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
- "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
- "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
- "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
- "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
- "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
- "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
- "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
- "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
- "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
- "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
- "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
- "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
- "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
- "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
- "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
- "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
- "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
- ],
- "version": "==1.1.1"
- },
- "numpy": {
- "hashes": [
- "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983",
- "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065",
- "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968",
- "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132",
- "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129",
- "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff",
- "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93",
- "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a",
- "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7",
- "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd",
- "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055",
- "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc",
- "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7",
- "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624",
- "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b",
- "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69",
- "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491",
- "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954",
- "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72",
- "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7",
- "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae",
- "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1",
- "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a",
- "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e",
- "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e",
- "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"
- ],
- "version": "==1.19.1"
- },
- "pandas": {
- "hashes": [
- "sha256:01b1e536eb960822c5e6b58357cad8c4b492a336f4a5630bf0b598566462a578",
- "sha256:0246c67cbaaaac8d25fed8d4cf2d8897bd858f0e540e8528a75281cee9ac516d",
- "sha256:0366150fe8ee37ef89a45d3093e05026b5f895e42bbce3902ce3b6427f1b8471",
- "sha256:16ae070c47474008769fc443ac765ffd88c3506b4a82966e7a605592978896f9",
- "sha256:1acc2bd7fc95e5408a4456897c2c2a1ae7c6acefe108d90479ab6d98d34fcc3d",
- "sha256:391db82ebeb886143b96b9c6c6166686c9a272d00020e4e39ad63b792542d9e2",
- "sha256:41675323d4fcdd15abde068607cad150dfe17f7d32290ee128e5fea98442bd09",
- "sha256:53328284a7bb046e2e885fd1b8c078bd896d7fc4575b915d4936f54984a2ba67",
- "sha256:57c5f6be49259cde8e6f71c2bf240a26b071569cabc04c751358495d09419e56",
- "sha256:84c101d0f7bbf0d9f1be9a2f29f6fcc12415442558d067164e50a56edfb732b4",
- "sha256:88930c74f69e97b17703600233c0eaf1f4f4dd10c14633d522724c5c1b963ec4",
- "sha256:8c9ec12c480c4d915e23ee9c8a2d8eba8509986f35f307771045c1294a2e5b73",
- "sha256:a81c4bf9c59010aa3efddbb6b9fc84a9b76dc0b4da2c2c2d50f06a9ef6ac0004",
- "sha256:d9644ac996149b2a51325d48d77e25c911e01aa6d39dc1b64be679cd71f683ec",
- "sha256:e4b6c98f45695799990da328e6fd7d6187be32752ed64c2f22326ad66762d179",
- "sha256:fe6f1623376b616e03d51f0dd95afd862cf9a33c18cf55ce0ed4bbe1c4444391"
- ],
- "index": "pypi",
- "version": "==1.1.1"
- },
- "plotly": {
- "hashes": [
- "sha256:257f530ffd73754bd008454826905657b329053364597479bb9774437a396837",
- "sha256:7c7b613af8def74f48b1298f08a49086bb9792104119feb4a137f737533021e1"
- ],
- "version": "==4.9.0"
- },
- "python-dateutil": {
- "hashes": [
- "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
- "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
- ],
- "version": "==2.8.1"
- },
- "python-dotenv": {
- "hashes": [
- "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
- "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
- ],
- "index": "pypi",
- "version": "==0.14.0"
- },
- "pytz": {
- "hashes": [
- "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
- "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
- ],
- "version": "==2020.1"
- },
- "retrying": {
- "hashes": [
- "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b"
- ],
- "version": "==1.3.3"
- },
- "six": {
- "hashes": [
- "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
- "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
- ],
- "version": "==1.15.0"
- },
- "webassets": {
- "hashes": [
- "sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
- "sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
- ],
- "version": "==2.0"
- },
- "werkzeug": {
- "hashes": [
- "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
- "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
- ],
- "version": "==1.0.1"
- }
- },
- "develop": {
- "cssmin": {
- "hashes": [
- "sha256:e012f0cc8401efcf2620332339011564738ae32be8c84b2e43ce8beaec1067b6"
- ],
- "index": "pypi",
- "version": "==0.2.0"
- },
- "lesscpy": {
- "hashes": [
- "sha256:7b664f60818a16afa8cc9f1dd6d9b17f944e0ce94e50787d76f81bc7a8648cce",
- "sha256:b0f2f853ee1dfb0891b147b57028057d5389510e079581e7b533d07dc0d95d3e"
- ],
- "index": "pypi",
- "version": "==0.14.0"
- },
- "ply": {
- "hashes": [
- "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3",
- "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"
- ],
- "version": "==3.11"
- },
- "six": {
- "hashes": [
- "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
- "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
- ],
- "version": "==1.15.0"
- }
- }
-}
diff --git a/README.md b/README.md
index 482b6d0..4841a08 100644
--- a/README.md
+++ b/README.md
@@ -1,72 +1,50 @@
# Plotly Dash Flask Tutorial
-
-
-
+
+

-
-
-
+
+
+

[](https://github.com/toddbirchard/plotlydash-flask-tutorial/issues)
[](https://github.com/toddbirchard/plotlydash-flask-tutorial/stargazers)
[](https://github.com/toddbirchard/plotlydash-flask-tutorial/network)
-
+
Make Plotly Dash part of your Flask Application by following this example.
-* The full tutorial can be found here: https://hackersandslackers.com/plotly-dash-with-flask/
-* A live demo can be found here: https://plotlydashflask.hackersandslackers.app/
+* **Tutorial**: https://hackersandslackers.com/plotly-dash-with-flask/
+* **Demo**: https://plotlydashflask.hackersandslackers.app/
-## Installation
+## Getting Started
-**Installation via `requirements.txt`**:
+Get set up locally in two steps:
-```shell
-$ git clone https://github.com/toddbirchard/plotlydash-flask-tutorial.git
-$ cd plotlydash-flask-tutorial
-$ python3 -m venv myenv
-$ source myenv/bin/activate
-$ pip3 install -r requirements.txt
-$ flask run
-```
+### Environment Variables
-**Installation via [Pipenv](https://pipenv-fork.readthedocs.io/en/latest/)**:
+Replace the values in **.env.example** with your values and rename this file to **.env**:
-```shell
-$ git clone https://github.com/toddbirchard/plotlydash-flask-tutorial.git
-$ cd plotlydash-flask-tutorial
-$ pipenv shell
-$ pipenv update
-$ flask run
-```
+* `FLASK_ENV`: The environment in which to run your application; either `development` or `production`.
+* `SECRET_KEY`: Randomly generated string of characters used to encrypt your app's data.
+* `LESS_BIN` *(optional for static assets)*: Path to your local LESS installation via `which lessc`.
+* `ASSETS_DEBUG` *(optional)*: Debug asset creation and bundling in `development`.
+* `LESS_RUN_IN_DEBUG` *(optional)*: Debug LESS while in `development`.
+* `COMPRESSOR_DEBUG` *(optional)*: Debug asset compression while in `development`.
+
+*Remember never to commit secrets saved in .env files to Github.*
-**Installation via [Poetry](https://python-poetry.org/)**:
+### Installation
+
+Get up and running with `make deploy`:
```shell
$ git clone https://github.com/hackersandslackers/plotlydash-flask-tutorial.git
-$ cd plotlydash-flask-tutorial
-$ poetry shell
-$ poetry update
-$ poetry run
+$ cd dashboard-flask-tutorial
+$ make deploy
```
-## Usage
-
-Replace the values in **.env.example** with your values and rename this file to **.env**:
-
-* `FLASK_APP`: Entry point of your application (should be `wsgi.py`).
-* `FLASK_ENV`: The environment to run your app in (either `development` or `production`).
-* `SECRET_KEY`: Randomly generated string of characters used to encrypt your app's data.
-* `LESS_BIN`: Path to your local LESS installation via `which lessc` (optional for static assets).
-* `ASSETS_DEBUG`: Debug asset creation and bundling in `development` (optional).
-* `LESS_RUN_IN_DEBUG`: Debug LESS while in `development` (optional).
-* `COMPRESSOR_DEBUG`: Debug asset compression while in `development` (optional).
-
-
-*Remember never to commit secrets saved in .env files to Github.*
-
-----
-**Hackers and Slackers** tutorials are free of charge. If you found this tutorial helpful, a [small donation](https://www.buymeacoffee.com/hackersslackers) would be greatly appreciated to keep us in business. All proceeds go towards coffee, and all coffee goes towards more content.
\ No newline at end of file
+**Hackers and Slackers** tutorials are free of charge. If you found this tutorial helpful, a [small donation](https://www.buymeacoffee.com/hackersslackers) would be greatly appreciated to keep us in business. All proceeds go towards coffee, and all coffee goes towards more content.
diff --git a/config.py b/config.py
index 087e41c..13baa5b 100644
--- a/config.py
+++ b/config.py
@@ -1,25 +1,24 @@
"""Flask config."""
from os import environ, path
-from dotenv import load_dotenv
BASE_DIR = path.abspath(path.dirname(__file__))
-load_dotenv(path.join(BASE_DIR, '.env'))
class Config:
"""Flask configuration variables."""
# General Config
- FLASK_APP = environ.get('FLASK_APP')
- FLASK_ENV = environ.get('FLASK_ENV')
- SECRET_KEY = environ.get('SECRET_KEY')
+ ENVIRONMENT = environ.get("ENVIRONMENT")
+ FLASK_APP = "main.py"
+ FLASK_DEBUG = environ.get("FLASK_DEBUG")
+ SECRET_KEY = environ.get("SECRET_KEY")
# Assets
- LESS_BIN = environ.get('LESS_BIN')
- ASSETS_DEBUG = environ.get('ASSETS_DEBUG')
- LESS_RUN_IN_DEBUG = environ.get('LESS_RUN_IN_DEBUG')
+ LESS_BIN = environ.get("LESS_BIN")
+ ASSETS_DEBUG = environ.get("ASSETS_DEBUG")
+ LESS_RUN_IN_DEBUG = environ.get("LESS_RUN_IN_DEBUG")
# Static Assets
- STATIC_FOLDER = 'static'
- TEMPLATES_FOLDER = 'templates'
- COMPRESSOR_DEBUG = environ.get('COMPRESSOR_DEBUG')
+ STATIC_FOLDER = "static"
+ TEMPLATES_FOLDER = "templates"
+ COMPRESSOR_DEBUG = environ.get("COMPRESSOR_DEBUG")
diff --git a/data/311-calls.csv b/data/calls.csv
similarity index 100%
rename from data/311-calls.csv
rename to data/calls.csv
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..360e379
--- /dev/null
+++ b/main.py
@@ -0,0 +1,7 @@
+"""Application entry point."""
+from plotly_flask_tutorial import init_app
+
+app = init_app()
+
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=8083, debug=True, load_dotenv=True)
diff --git a/plotlyflask_tutorial/__init__.py b/plotly_flask_tutorial/__init__.py
similarity index 85%
rename from plotlyflask_tutorial/__init__.py
rename to plotly_flask_tutorial/__init__.py
index ec16605..abded56 100644
--- a/plotlyflask_tutorial/__init__.py
+++ b/plotly_flask_tutorial/__init__.py
@@ -6,7 +6,7 @@
def init_app():
"""Construct core Flask application with embedded Dash app."""
app = Flask(__name__, instance_relative_config=False)
- app.config.from_object('config.Config')
+ app.config.from_object("config.Config")
assets = Environment()
assets.init_app(app)
@@ -16,7 +16,8 @@ def init_app():
from .assets import compile_static_assets
# Import Dash application
- from .plotlydash.dashboard import init_dashboard
+ from .dashboard import init_dashboard
+
app = init_dashboard(app)
# Compile static assets
diff --git a/plotlyflask_tutorial/assets.py b/plotly_flask_tutorial/assets.py
similarity index 63%
rename from plotlyflask_tutorial/assets.py
rename to plotly_flask_tutorial/assets.py
index 4ac0b07..4dad152 100644
--- a/plotlyflask_tutorial/assets.py
+++ b/plotly_flask_tutorial/assets.py
@@ -13,12 +13,12 @@ def compile_static_assets(assets):
assets.auto_build = True
assets.debug = False
less_bundle = Bundle(
- 'less/*.less',
- filters='less,cssmin',
- output='dist/css/styles.css',
- extra={'rel': 'stylesheet/less'}
+ "less/*.less",
+ filters="less,cssmin",
+ output="dist/css/styles.css",
+ extra={"rel": "stylesheet/less"},
)
- assets.register('less_all', less_bundle)
- if app.config['FLASK_ENV'] == 'development':
+ assets.register("less_all", less_bundle)
+ if app.config["ENVIRONMENT"] == "development":
less_bundle.build()
return assets
diff --git a/plotly_flask_tutorial/dashboard/__init__.py b/plotly_flask_tutorial/dashboard/__init__.py
new file mode 100644
index 0000000..6acb08e
--- /dev/null
+++ b/plotly_flask_tutorial/dashboard/__init__.py
@@ -0,0 +1,78 @@
+"""Instantiate a Dash app."""
+import dash
+from dash import dcc, html
+from dash.dash_table import DataTable
+from flask import Flask
+from pandas import DataFrame
+
+from .data import create_dataframe
+from .layout import html_layout
+
+
+def init_dashboard(app: Flask):
+ """
+ Create a Plotly Dash dashboard within a running Flask app.
+
+ :param Flask app: Top-level Flask application.
+ """
+ dash_module = dash.Dash(
+ server=app,
+ routes_pathname_prefix="/dashapp/",
+ external_stylesheets=[
+ "/static/dist/css/styles.css",
+ "https://fonts.googleapis.com/css?family=Lato",
+ ],
+ )
+
+ # Load DataFrame
+ df = create_dataframe()
+
+ # Custom HTML layout
+ dash_module.index_string = html_layout
+
+ # Create Layout
+ dash_module.layout = html.Div(
+ children=[
+ dcc.Graph(
+ id="histogram-graph",
+ figure={
+ "data": [
+ {
+ "x": df["complaint_type"],
+ "text": df["complaint_type"],
+ "customdata": df["key"],
+ "name": "311 Calls by region.",
+ "type": "histogram",
+ }
+ ],
+ "layout": {
+ "title": "NYC 311 Calls category.",
+ "height": 500,
+ "padding": 150,
+ },
+ },
+ ),
+ create_data_table(df),
+ ],
+ id="dash-container",
+ )
+ return dash_module.server
+
+
+def create_data_table(df: DataFrame) -> DataTable:
+ """
+ Create Dash DataTable object from Pandas DataFrame.
+
+ :param DataFrame df: Pandas DataFrame from which to build a Dash table.
+
+ :returns: DataTable
+ """
+ table = DataTable(
+ id="database-table",
+ columns=[{"name": i, "id": i} for i in df.columns],
+ data=df.to_dict("records"),
+ sort_action="native",
+ sort_mode="native",
+ page_size=300,
+ )
+ return table
diff --git a/plotly_flask_tutorial/dashboard/data.py b/plotly_flask_tutorial/dashboard/data.py
new file mode 100644
index 0000000..bf6c820
--- /dev/null
+++ b/plotly_flask_tutorial/dashboard/data.py
@@ -0,0 +1,15 @@
+"""Prepare data for Plotly Dash."""
+import numpy as np
+import pandas as pd
+from pandas import DataFrame
+
+
+def create_dataframe() -> DataFrame:
+ """Create Pandas DataFrame from local CSV."""
+ df = pd.read_csv("data/calls.csv", parse_dates=["created"])
+ df["created"] = df["created"].dt.date
+ df.drop(columns=["incident_zip"], inplace=True)
+ num_complaints = df["complaint_type"].value_counts()
+ to_remove = num_complaints[num_complaints <= 30].index
+ df.replace(to_remove, np.nan, inplace=True)
+ return df
diff --git a/plotlyflask_tutorial/plotlydash/layout.py b/plotly_flask_tutorial/dashboard/layout.py
similarity index 87%
rename from plotlyflask_tutorial/plotlydash/layout.py
rename to plotly_flask_tutorial/dashboard/layout.py
index b211ac7..5c0a1d2 100644
--- a/plotlyflask_tutorial/plotlydash/layout.py
+++ b/plotly_flask_tutorial/dashboard/layout.py
@@ -1,6 +1,6 @@
"""Plotly Dash HTML layout override."""
-html_layout = '''
+html_layout = """
@@ -13,7 +13,7 @@
-
+
Plotly Dash Flask Tutorial