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 - -![Python](https://img.shields.io/badge/Python-^3.8-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a) -![Flask](https://img.shields.io/badge/Flask-1.1.2-blue.svg?longCache=true&logo=flask&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) +![Python](https://img.shields.io/badge/Python-^3.10-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a) +![Flask](https://img.shields.io/badge/Flask^2.2.5-blue.svg?longCache=true&logo=flask&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) ![Flask-Assets](https://img.shields.io/badge/Flask--Assets-v2.0-blue.svg?longCache=true&logo=flask&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) -![Pandas](https://img.shields.io/badge/Pandas-v^1.0.0-blue.svg?longCache=true&logo=python&longCache=true&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) -![Dash](https://img.shields.io/badge/Dash-v1.12.0-blue.svg?longCache=true&logo=python&longCache=true&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) -![Plotly](https://img.shields.io/badge/Plotly-v4.8.1-blue.svg?longCache=true&logo=python&longCache=true&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) +![Pandas](https://img.shields.io/badge/Pandas-v^2.1.0-blue.svg?longCache=true&logo=python&longCache=true&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) +![Dash](https://img.shields.io/badge/Dash-v^2.13.0-blue.svg?longCache=true&logo=python&longCache=true&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) +![Plotly](https://img.shields.io/badge/Plotly-v^5.16.1-blue.svg?longCache=true&logo=python&longCache=true&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) ![GitHub Last Commit](https://img.shields.io/github/last-commit/google/skia.svg?style=flat-square&colorA=4c566a&colorB=a3be8c) [![GitHub Issues](https://img.shields.io/github/issues/toddbirchard/plotlydash-flask-tutorial.svg?style=flat-square&colorA=4c566a&colorB=ebcb8b)](https://github.com/toddbirchard/plotlydash-flask-tutorial/issues) [![GitHub Stars](https://img.shields.io/github/stars/toddbirchard/plotlydash-flask-tutorial.svg?style=flat-square&colorB=ebcb8b&colorA=4c566a)](https://github.com/toddbirchard/plotlydash-flask-tutorial/stargazers) [![GitHub Forks](https://img.shields.io/github/forks/toddbirchard/plotlydash-flask-tutorial.svg?style=flat-square&colorA=4c566a&colorB=ebcb8b)](https://github.com/toddbirchard/plotlydash-flask-tutorial/network) -![Plotly Dash Tutorial](https://storage.googleapis.com/hackersandslackers-cdn/2018/12/Dash@2x.jpg) +![Plotly Dash Tutorial](./.github/dash@2x.jpg?raw=true) 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 @@