├── .gitignore ├── .idea ├── deploy-dash-with-gcp.iml ├── encodings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── Dockerfile ├── README.md ├── app ├── .gcloudignore ├── __init__.py ├── app.yaml ├── assets │ ├── base.css │ ├── dsc-logo2.png │ └── dsc.css ├── main.py └── requirements.txt ├── images ├── add_people.png ├── dash_app.png ├── members.png └── new_project.png ├── simple-dash-app-engine-app ├── .gcloudignore ├── README.md ├── app.yaml ├── assets │ ├── base.css │ ├── dsc-logo2.png │ └── dsc.css ├── data │ └── data.csv ├── main.py └── requirements.txt └── simple-dash-app-using-a-bucket ├── .gcloudignore ├── README.md ├── __init__.py ├── app.yaml ├── assets ├── base.css ├── dsc-logo2.png └── dsc.css ├── data ├── __init__.py ├── data.csv ├── dataDownloader.py └── dataUpload.py ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # History files 2 | .Rhistory 3 | .Rapp.history 4 | .Rproj.user 5 | .RData 6 | .Ruserdata 7 | .DS_Store 8 | .gitmodules 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # config 19 | config.json 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Environments 97 | .env 98 | .venv 99 | env/ 100 | venv/ 101 | ENV/ 102 | env.bak/ 103 | venv.bak/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | 118 | # Directories 119 | *.json 120 | output/aligned_images 121 | data/key/* -------------------------------------------------------------------------------- /.idea/deploy-dash-with-gcp.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Move to /dash 2 | 3 | FROM python:3.6 4 | 5 | WORKDIR / 6 | 7 | COPY . / 8 | 9 | RUN pip install --trusted-host pypi.python.org -r requirements.txt 10 | 11 | EXPOSE 8000 12 | 13 | ENTRYPOINT ["gunicorn","--bind=0.0.0.0:8080","main:server"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy Dash with GCP 2 | 3 | This repository contains two simple dash applications that can be deployed with Google Cloud Platform (GCP). 4 | 5 | 1. The first is one that reads in a locally-stored file in `data` folder and creates a graph from adding and multiplying the numbers in `data.csv`. This can be found in the `/simple-dash-app-engine-app` folder, including a [README](https://github.com/datasciencecampus/deploy-dash-with-gcp/blob/master/simple-dash-app-engine-app/README.md) guide. 6 | 2. The second is an example whereby we link the app to a GCP bucket containing the data. This can be found in the `/simple-dash-app-using-a-bucket` folder. This also has a [README](https://github.com/datasciencecampus/deploy-dash-with-gcp/blob/master/simple-dash-app-using-a-bucket/README.md) guide. 7 | 8 | More training materials can be found on our [GitHub pages training page](https://datasciencecampus.github.io/training/). 9 | 10 | To run these examples, clone this repository using: 11 | 12 | ``` 13 | git clone https://github.com/datasciencecampus/deploy-dash-with-gcp.git 14 | ``` 15 | -------------------------------------------------------------------------------- /app/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Python pycache: 17 | __pycache__/ 18 | # Ignored by the build system 19 | /setup.cfg 20 | 21 | # images 22 | /images/* 23 | 24 | # key 25 | data/key/* -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/app/__init__.py -------------------------------------------------------------------------------- /app/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python37 2 | 3 | basic_scaling: 4 | max_instances: 2 5 | idle_timeout: 10m 6 | 7 | resources: 8 | cpu: 1 9 | memory_gb: 1 10 | disk_size_gb: 10 11 | 12 | entrypoint: gunicorn -b 0.0.0.0:8080 main:server 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/assets/base.css: -------------------------------------------------------------------------------- 1 | /* Table of contents 2 | –––––––––––––––––––––––––––––––––––––––––––––––––– 3 | - Plotly.js 4 | - Grid 5 | - Base Styles 6 | - Typography 7 | - Links 8 | - Buttons 9 | - Forms 10 | - Lists 11 | - Code 12 | - Tables 13 | - Spacing 14 | - Utilities 15 | - Clearing 16 | - Media Queries 17 | */ 18 | 19 | 20 | /* Grid 21 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 22 | .container { 23 | position: relative; 24 | width: 100%; 25 | max-width: 960px; 26 | margin: 0 auto; 27 | padding: 0 20px; 28 | box-sizing: border-box; } 29 | .column, 30 | .columns { 31 | width: 100%; 32 | float: left; 33 | box-sizing: border-box; } 34 | 35 | /* For devices larger than 400px */ 36 | @media (min-width: 400px) { 37 | .container { 38 | width: 85%; 39 | padding: 0; } 40 | } 41 | 42 | /* For devices larger than 550px */ 43 | @media (min-width: 550px) { 44 | .container { 45 | width: 80%; } 46 | .column, 47 | .columns { 48 | margin-left: 2%; } 49 | .column:first-child, 50 | .columns:first-child { 51 | margin-left: 1%; } 52 | 53 | .one.column, 54 | .one.columns { width: 4.66666666667%; } 55 | .two.columns { width: 13.3333333333%; } 56 | .three.columns { width: 22%; } 57 | .four.columns { width: 30.6666666667%; } 58 | .five.columns { width: 39.3333333333%; } 59 | .six.columns { width: 48%; } 60 | .seven.columns { width: 56.6666666667%; } 61 | .eight.columns { width: 65.3333333333%; } 62 | .nine.columns { width: 74.0%; } 63 | .ten.columns { width: 82.6666666667%; } 64 | .eleven.columns { width: 91.3333333333%; } 65 | .twelve.columns { width: 100%; margin-left: 0; } 66 | 67 | .one-third.column { width: 30.6666666667%; } 68 | .two-thirds.column { width: 65.3333333333%; } 69 | 70 | .one-half.column { width: 48%; } 71 | 72 | /* Offsets */ 73 | .offset-by-one.column, 74 | .offset-by-one.columns { margin-left: 8.66666666667%; } 75 | .offset-by-two.column, 76 | .offset-by-two.columns { margin-left: 17.3333333333%; } 77 | .offset-by-three.column, 78 | .offset-by-three.columns { margin-left: 26%; } 79 | .offset-by-four.column, 80 | .offset-by-four.columns { margin-left: 34.6666666667%; } 81 | .offset-by-five.column, 82 | .offset-by-five.columns { margin-left: 43.3333333333%; } 83 | .offset-by-six.column, 84 | .offset-by-six.columns { margin-left: 52%; } 85 | .offset-by-seven.column, 86 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 87 | .offset-by-eight.column, 88 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 89 | .offset-by-nine.column, 90 | .offset-by-nine.columns { margin-left: 78.0%; } 91 | .offset-by-ten.column, 92 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 93 | .offset-by-eleven.column, 94 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 95 | 96 | .offset-by-one-third.column, 97 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 98 | .offset-by-two-thirds.column, 99 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 100 | 101 | .offset-by-one-half.column, 102 | .offset-by-one-half.columns { margin-left: 52%; } 103 | 104 | } 105 | 106 | 107 | /* Base Styles 108 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 109 | /* NOTE 110 | html is set to 62.5% so that all the REM measurements throughout Skeleton 111 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 112 | html { 113 | font-size: 62.5%; } 114 | 115 | body { 116 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 117 | line-height: 1.6; 118 | font-weight: 400; 119 | font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 120 | color: rgb(50, 50, 50); 121 | margin: 0; 122 | } 123 | 124 | 125 | /* Typography 126 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 127 | h1, h2, h3, h4, h5, h6 { 128 | margin-top: 0; 129 | margin-bottom: 0; 130 | font-weight: 300; } 131 | h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } 132 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} 133 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} 134 | h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} 135 | h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} 136 | h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} 137 | 138 | p { 139 | margin-top: 0; } 140 | 141 | 142 | /* Blockquotes 143 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 144 | blockquote { 145 | border-left: 4px lightgrey solid; 146 | padding-left: 1rem; 147 | margin-top: 2rem; 148 | margin-bottom: 2rem; 149 | margin-left: 0rem; 150 | } 151 | 152 | 153 | /* Links 154 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 155 | a { 156 | color: #1EAEDB; 157 | text-decoration: underline; 158 | cursor: pointer;} 159 | a:hover { 160 | color: #0FA0CE; } 161 | 162 | 163 | /* Buttons 164 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 165 | .button, 166 | button, 167 | input[type="submit"], 168 | input[type="reset"], 169 | input[type="button"] { 170 | display: inline-block; 171 | height: 38px; 172 | padding: 0 30px; 173 | color: #555; 174 | text-align: center; 175 | font-size: 11px; 176 | font-weight: 600; 177 | line-height: 38px; 178 | letter-spacing: .1rem; 179 | text-transform: uppercase; 180 | text-decoration: none; 181 | white-space: nowrap; 182 | background-color: transparent; 183 | border-radius: 4px; 184 | border: 1px solid #bbb; 185 | cursor: pointer; 186 | box-sizing: border-box; } 187 | .button:hover, 188 | button:hover, 189 | input[type="submit"]:hover, 190 | input[type="reset"]:hover, 191 | input[type="button"]:hover, 192 | .button:focus, 193 | button:focus, 194 | input[type="submit"]:focus, 195 | input[type="reset"]:focus, 196 | input[type="button"]:focus { 197 | color: #333; 198 | border-color: #888; 199 | outline: 0; } 200 | .button.button-primary, 201 | button.button-primary, 202 | input[type="submit"].button-primary, 203 | input[type="reset"].button-primary, 204 | input[type="button"].button-primary { 205 | color: #FFF; 206 | background-color: #33C3F0; 207 | border-color: #33C3F0; } 208 | .button.button-primary:hover, 209 | button.button-primary:hover, 210 | input[type="submit"].button-primary:hover, 211 | input[type="reset"].button-primary:hover, 212 | input[type="button"].button-primary:hover, 213 | .button.button-primary:focus, 214 | button.button-primary:focus, 215 | input[type="submit"].button-primary:focus, 216 | input[type="reset"].button-primary:focus, 217 | input[type="button"].button-primary:focus { 218 | color: #FFF; 219 | background-color: #1EAEDB; 220 | border-color: #1EAEDB; } 221 | 222 | 223 | /* Forms 224 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 225 | input[type="email"], 226 | input[type="number"], 227 | input[type="search"], 228 | input[type="text"], 229 | input[type="tel"], 230 | input[type="url"], 231 | input[type="password"], 232 | textarea, 233 | select { 234 | height: 38px; 235 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 236 | background-color: #fff; 237 | border: 1px solid #D1D1D1; 238 | border-radius: 4px; 239 | box-shadow: none; 240 | box-sizing: border-box; 241 | font-family: inherit; 242 | font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} 243 | /* Removes awkward default styles on some inputs for iOS */ 244 | input[type="email"], 245 | input[type="number"], 246 | input[type="search"], 247 | input[type="text"], 248 | input[type="tel"], 249 | input[type="url"], 250 | input[type="password"], 251 | textarea { 252 | -webkit-appearance: none; 253 | -moz-appearance: none; 254 | appearance: none; } 255 | textarea { 256 | min-height: 65px; 257 | padding-top: 6px; 258 | padding-bottom: 6px; } 259 | input[type="email"]:focus, 260 | input[type="number"]:focus, 261 | input[type="search"]:focus, 262 | input[type="text"]:focus, 263 | input[type="tel"]:focus, 264 | input[type="url"]:focus, 265 | input[type="password"]:focus, 266 | textarea:focus, 267 | /*select:focus {*/ 268 | /* border: 1px solid #33C3F0;*/ 269 | /* outline: 0; }*/ 270 | label, 271 | legend { 272 | display: block; 273 | margin-bottom: 0px; } 274 | fieldset { 275 | padding: 0; 276 | border-width: 0; } 277 | input[type="checkbox"], 278 | input[type="radio"] { 279 | display: inline; } 280 | label > .label-body { 281 | display: inline-block; 282 | margin-left: .5rem; 283 | font-weight: normal; } 284 | 285 | 286 | /* Lists 287 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 288 | ul { 289 | list-style: circle inside; } 290 | ol { 291 | list-style: decimal inside; } 292 | ol, ul { 293 | padding-left: 0; 294 | margin-top: 0; } 295 | ul ul, 296 | ul ol, 297 | ol ol, 298 | ol ul { 299 | margin: 1.5rem 0 1.5rem 3rem; 300 | font-size: 90%; } 301 | li { 302 | margin-bottom: 1rem; } 303 | 304 | 305 | /* Tables 306 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 307 | table { 308 | border-collapse: collapse; 309 | } 310 | th, 311 | td { 312 | padding: 12px 15px; 313 | text-align: left; 314 | border-bottom: 1px solid #E1E1E1; } 315 | th:first-child, 316 | td:first-child { 317 | padding-left: 0; } 318 | th:last-child, 319 | td:last-child { 320 | padding-right: 0; } 321 | 322 | 323 | /* Spacing 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | button, 326 | .button { 327 | margin-bottom: 0rem; } 328 | input, 329 | textarea, 330 | select, 331 | fieldset { 332 | margin-bottom: 0rem; } 333 | pre, 334 | dl, 335 | figure, 336 | table, 337 | form { 338 | margin-bottom: 0rem; } 339 | p, 340 | ul, 341 | ol { 342 | margin-bottom: 0.75rem; } 343 | 344 | /* Utilities 345 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 346 | .u-full-width { 347 | width: 100%; 348 | box-sizing: border-box; } 349 | .u-max-full-width { 350 | max-width: 100%; 351 | box-sizing: border-box; } 352 | .u-pull-right { 353 | float: right; } 354 | .u-pull-left { 355 | float: left; } 356 | 357 | 358 | /* Misc 359 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 360 | hr { 361 | margin-top: 3rem; 362 | margin-bottom: 3.5rem; 363 | border-width: 0; 364 | border-top: 1px solid #E1E1E1; } 365 | 366 | 367 | /* Clearing 368 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 369 | 370 | /* Self Clearing Goodness */ 371 | .container:after, 372 | .row:after, 373 | .u-cf { 374 | content: ""; 375 | display: table; 376 | clear: both; } 377 | 378 | 379 | /* Media Queries 380 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 381 | /* 382 | Note: The best way to structure the use of media queries is to create the queries 383 | near the relevant code. For example, if you wanted to change the styles for buttons 384 | on small devices, paste the mobile query code up in the buttons section and style it 385 | there. 386 | */ 387 | 388 | 389 | /* Larger than mobile */ 390 | @media (min-width: 400px) {} 391 | 392 | /* Larger than phablet (also point when grid becomes active) */ 393 | @media (min-width: 550px) {} 394 | 395 | /* Larger than tablet */ 396 | @media (min-width: 750px) {} 397 | 398 | /* Larger than desktop */ 399 | @media (min-width: 1000px) {} 400 | 401 | /* Larger than Desktop HD */ 402 | @media (min-width: 1200px) {} 403 | -------------------------------------------------------------------------------- /app/assets/dsc-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/app/assets/dsc-logo2.png -------------------------------------------------------------------------------- /app/assets/dsc.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,800'); 2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700'); 3 | 4 | .container.scalable { 5 | width: 95%; 6 | max-width: none; 7 | } 8 | 9 | @media print { 10 | body {-webkit-print-color-adjust: exact;} 11 | } 12 | 13 | /* Remove Undo 14 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 15 | ._dash-undo-redo { 16 | display: none; 17 | } 18 | 19 | 20 | @media screen { 21 | 22 | .banner Img { 23 | height: 5rem; 24 | margin-bottom: 1rem; 25 | } 26 | 27 | #instructions { 28 | font-size: 2rem; 29 | font-family: 'Open Sans', sans-serif; 30 | font-weight: 300; 31 | color: #001d4d; 32 | margin: 0rem 0rem 2rem ; 33 | } 34 | } 35 | 36 | @media print { 37 | 38 | #instructions { 39 | font-size: 2rem; 40 | font-family: 'Open Sans', sans-serif; 41 | font-weight: 300; 42 | color: #001d4d; 43 | margin: 0rem 0rem 2rem ; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import dash 2 | import dash_core_components as dcc 3 | import dash_html_components as html 4 | import pandas as pd 5 | from io import StringIO 6 | import os 7 | 8 | from data.dataDownloader import GCPDownloaderLocal, GCPDownloaderCloud 9 | 10 | # -------------------------- PYTHON FUNCTIONS ---------------------------- # 11 | 12 | 13 | def add_numbers(first_num,second_num): 14 | new_num = first_num + second_num 15 | return new_num 16 | 17 | def multiply_numbers(first_num,second_num): 18 | new_num = first_num * second_num 19 | return new_num 20 | 21 | 22 | def build_banner(): 23 | return html.Div( 24 | id='banner', 25 | className='banner', 26 | children=[ 27 | html.Img(src=app.get_asset_url('dsc-logo2.png')), 28 | ], 29 | ) 30 | 31 | def data_in(): 32 | 33 | if cloud == False: 34 | data = os.path.join('data/data.csv') 35 | 36 | else: 37 | project = 'dash-example-265811' 38 | project_name = 'dash-example-265811.appspot.com' 39 | folder_name = 'data' 40 | file_name = 'data.csv' 41 | 42 | if local == True: 43 | GCP = GCPDownloaderLocal() # run locally 44 | else: 45 | GCP = GCPDownloaderCloud() # run on cloud 46 | 47 | bytes_file = GCP.getData(project, project_name, folder_name, file_name) 48 | s = str(bytes_file, encoding='utf-8') 49 | data = StringIO(s) 50 | 51 | data_df = pd.read_csv(data) 52 | 53 | add_num_list = [] 54 | multiply_num_list = [] 55 | 56 | for index, row in data_df.iterrows(): 57 | add_num_list.append(add_numbers(row['first_num'], row['second_num'])) 58 | multiply_num_list.append(multiply_numbers(row['first_num'], row['second_num'])) 59 | 60 | data_df['add_num'] = add_num_list 61 | data_df['multiply_num'] = multiply_num_list 62 | 63 | return data_df 64 | 65 | def app_layout(): 66 | data_df = data_in() 67 | return html.Div(children=[ 68 | html.H1( 69 | children=[ 70 | build_banner(), 71 | html.P( 72 | id='instructions', 73 | children=dash_text), 74 | ] 75 | ), 76 | 77 | dcc.Graph( 78 | id='example-graph', 79 | figure={ 80 | 'data': [ 81 | {'x': data_df.index.values.tolist(), 'y': data_df['add_num'], 'type': 'bar', 'name': 'Add Numbers'}, 82 | {'x': data_df.index.values.tolist(), 'y': data_df['multiply_num'], 'type': 'bar', 'name': 'Multiply Numbers'}, 83 | ], 84 | 'layout': { 85 | 'title': 'Dash Data Visualization' 86 | } 87 | } 88 | ) 89 | ]) 90 | 91 | 92 | # -------------------------- TEXT ---------------------------- # 93 | 94 | 95 | dash_text = ''' 96 | 97 | This is an example of a DSC dashboard. 98 | ''' 99 | 100 | 101 | # -------------------------- DASH ---------------------------- # 102 | 103 | 104 | external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] 105 | 106 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets, assets_folder='assets') 107 | server = app.server 108 | 109 | local = False 110 | cloud = True 111 | 112 | data_df = data_in() 113 | 114 | app.layout = app_layout 115 | app.config.suppress_callback_exceptions = True 116 | 117 | 118 | # -------------------------- MAIN ---------------------------- # 119 | 120 | 121 | if __name__ == '__main__': 122 | app.run_server(host='0.0.0.0', port=8080, debug=True, use_reloader=False) -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | dash==1.6.0 3 | dash-core-components==1.5.0 4 | dash-html-components==1.0.1 5 | dash-renderer==1.2.0 6 | Flask==1.1.1 7 | Flask-Compress==1.4.0 8 | future==0.18.2 9 | itsdangerous==1.1.0 10 | Jinja2==2.10.3 11 | MarkupSafe==1.1.1 12 | numpy==1.16.5 13 | pandas==0.24.2 14 | pytz==2019.3 15 | retrying==1.3.3 16 | six==1.13.0 17 | Werkzeug==0.16.0 18 | gunicorn>=19.5.0 19 | google-api-core==1.16.0 20 | google-auth==1.10.1 21 | google-cloud-core==1.2.0 22 | google-cloud-storage==1.25.0 23 | google-resumable-media==0.5.0 24 | googleapis-common-protos==1.51.0 -------------------------------------------------------------------------------- /images/add_people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/images/add_people.png -------------------------------------------------------------------------------- /images/dash_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/images/dash_app.png -------------------------------------------------------------------------------- /images/members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/images/members.png -------------------------------------------------------------------------------- /images/new_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/images/new_project.png -------------------------------------------------------------------------------- /simple-dash-app-engine-app/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Python pycache: 17 | __pycache__/ 18 | # Ignored by the build system 19 | /setup.cfg 20 | 21 | # images 22 | /images/* 23 | 24 | # key 25 | data/key/* -------------------------------------------------------------------------------- /simple-dash-app-engine-app/README.md: -------------------------------------------------------------------------------- 1 | This guide has been created to help users deploy a dash application using Google Cloud Platform (GCP) App Engine, using a locally-stored file. For more information, visit the [GitHub repository](https://github.com/datasciencecampus/deploy-dash-with-gcp). 2 | 3 | *Note: This blog has been updated since it was first posted. See the Version control section at the bottom of the post for changes.* 4 | 5 | ## Step 1: Creating your Dash Application 6 | 7 | Visit our [GitHub repository](https://github.com/datasciencecampus/deploy-dash-with-gcp/tree/master/simple-dash-app-engine-app) to view all the files. 8 | 9 | Important files: 10 | 11 | * `main.py` is the Dash application 12 | * `.gcloudignore` is like `.gitignore` for GitHub, it tells GCP what not to upload (for example here, I don't want to upload all the screenshots used in this guide) 13 | * `app.yaml` is used to run the Dash app on GCP using [gunicorn](https://gunicorn.org/), which is needed for GCP 14 | * `requirements.txt` comprises the packages needed to run the Dash app (important: gunicorn is required in this file at the bare minimum) 15 | 16 | #### main.py 17 | 18 | The `main.py` python script comprises the following, which are split into sections below. The full script can be found at the bottom of this post. 19 | 20 | **Python Functions** 21 | 22 | This section has three functions: add numbers, multiply numbers and build banner. The first two are self-explinatory, the last just creates an image banner based on a file in the `assets` folder. 23 | 24 | **Load Data** 25 | 26 | This section loads the csv file from the `/data` folder, creates a pandas dataframe and then applies the add and multiply number functions. 27 | 28 | **Text** 29 | 30 | This section just contains free text to append to the Dash app. 31 | 32 | **Dash** 33 | 34 | This contains the Dash setup values, including the `/assets` folder. 35 | 36 | **Project Dashboard** 37 | 38 | This is the core of the Dash application. 39 | 40 | ``` 41 | app.layout = html.Div(children=[ 42 | html.H1( 43 | children=[ 44 | build_banner(), 45 | html.P( 46 | id='instructions', 47 | children=dash_text), 48 | ] 49 | ), 50 | 51 | dcc.Graph( 52 | id='example-graph', 53 | figure={ 54 | 'data': [ 55 | {'x': data_df.index.values.tolist(), 'y': data_df['add_num'], 'type': 'bar', 'name': 'Add Numbers'}, 56 | {'x': data_df.index.values.tolist(), 'y': data_df['multiply_num'], 'type': 'bar', 'name': 'Multiply Numbers'}, 57 | ], 58 | 'layout': { 59 | 'title': 'Dash Data Visualization' 60 | } 61 | } 62 | ) 63 | ]) 64 | ``` 65 | 66 | This simply creates the image banner, then adds the free text (which has an `id` whose style can be edited in the `/assets/dsc.css` file, then adds the graph based on the loaded and manipulated data from `/data/datacsv`. 67 | 68 | **main** 69 | 70 | ``` 71 | if __name__ == '__main__': 72 | app.server(host='0.0.0.0', port=8080, debug=True) 73 | ``` 74 | 75 | It is important you make a note of the port number and host! 76 | 77 | #### requirements.txt 78 | 79 | Here, this is the `requirements.txt` file, which tells GCP which packages to install. 80 | 81 | ``` 82 | Click==7.0 83 | dash==1.6.0 84 | dash-core-components==1.5.0 85 | dash-html-components==1.0.1 86 | dash-renderer==1.2.0 87 | Flask==1.1.1 88 | Flask-Compress==1.4.0 89 | future==0.18.2 90 | itsdangerous==1.1.0 91 | Jinja2==2.10.3 92 | MarkupSafe==1.1.1 93 | numpy==1.16.5 94 | pandas==0.24.2 95 | pytz==2019.3 96 | retrying==1.3.3 97 | six==1.13.0 98 | Werkzeug==0.16.0 99 | gunicorn==19.3.0 100 | ``` 101 | 102 | #### app.yaml 103 | 104 | This is a really important file, and needs to replicate what is put in your `main.py` script. 105 | 106 | ``` 107 | service: default 108 | runtime: python37 109 | 110 | basic_scaling: 111 | max_instances: 2 112 | idle_timeout: 10m 113 | 114 | resources: 115 | cpu: 1 116 | memory_gb: 1 117 | disk_size_gb: 10 118 | 119 | entrypoint: gunicorn -b 0.0.0.0:8080 main:server 120 | ``` 121 | 122 | This file tells GCP how to create the application. The first line specifies the service name, which becomes the prefix of the URL the app will run on. Yhe second line specifies that we want to build using python 3.7. The basic_scaling and resources blocks tell App Engine what the environment should be. Here we are limiting the instances to 2, on a machine with 1 CPU and 1 GB of RAM. The entrypoint line must replicate what is at the end of `main.py` (the host and port numbers), as well as what you call the python script (here `main.py` = `main`). This is the most likely file to corrupt a build. 123 | 124 | ## Step 2: Deploy your Application to Google Cloud Platform 125 | 126 | This guide builds on other guides such as [Jamie Phillips'](https://www.phillipsj.net/posts/deploying-dash-to-google-app-engine/). However, we were not able to successfully deploy a Dash app following Jamie's, or others, examples without heavy tweaks. We also include additional python functions to load data in our example here (Goodbye, World). 127 | 128 | The following steps are to deploy a Dash application to GCP. If your app doesn't work locally, you should fix that first as it won't work on GCP (even if you pray real hard). If it works locally, but it doesn't deploy, the majority of the time it will be due to the `app.yaml` file. 129 | 130 | ### Step 2.1: Make a Project on GCP 131 | 132 | Using the CLI or the Console Interface online (which we use below), create a new project with a suitable project name (here we call it `dash-example`). 133 | 134 | ![New Project in GCP](../../images/training/dash/new_project.png) 135 | 136 | ### Step 2.2: Make Yourself the Owner of Project 137 | 138 | Make sure the project you've just created is selected on the console, then click 'ADD PEOPLE TO THIS PROJECT'. 139 | 140 | ![Add People to Project in GCP](../../images/training/dash/add_people.png) 141 | 142 | Then input your user name and set the role to `Project` > `Owner`. 143 | 144 | ![Add Member Roles to Project in GCP](../../images/training/dash/members.png) 145 | 146 | That's it for now on the Google Cloud Platform Console. 147 | 148 | ### Step 2.3: Deploy Using gcloud Command Line Tool 149 | 150 | If you haven't installed the [gcloud command line tool](https://cloud.google.com/sdk/gcloud/) do so now. 151 | 152 | Next, check your project is active in gcloud using: 153 | 154 | `gcloud config get-value project` 155 | 156 | Which will print the following on screen: 157 | 158 | ``` 159 | Your active configuration is: [default] 160 | 161 | my-project-id 162 | ``` 163 | 164 | To change the project to your desired project, type: 165 | 166 | `gcloud config set project project-id` 167 | 168 | Next, to deploy, type: 169 | 170 | `gcloud app deploy` 171 | 172 | Then select your desired region (we use `europe-west2`, which is the London region) 173 | 174 | If you have setup your configuration correctly then it will deploy the Dash app (after a while), which will be available at: 175 | 176 | `https://project-id.appspot.com/` 177 | 178 | The example app above is hosted [here](https://simple-dash-app-engine-app-dot-dash-example-265811.appspot.com/). 179 | 180 | Visit our [GitHub repository](https://github.com/datasciencecampus/deploy-dash-with-gcp/tree/master/simple-dash-app-engine-app) to view all the files. 181 | 182 | ## Step 3: Restrict Access to your Application 183 | 184 | By default your application will be accessible to anyone in the world. To restrict the access you can use [Firewall Rules](https://cloud.google.com/blog/products/gcp/introducing-app-engine-firewall-an-easy-way-to-control-access-to-your-app). 185 | 186 | ## main.py script 187 | 188 | ``` 189 | import dash 190 | import dash_core_components as dcc 191 | import dash_html_components as html 192 | import pandas as pd 193 | import os 194 | 195 | 196 | # -------------------------- PYTHON FUNCTIONS ---------------------------- # 197 | 198 | 199 | def add_numbers(first_num,second_num): 200 | new_num = first_num + second_num 201 | return new_num 202 | 203 | def multiply_numbers(first_num,second_num): 204 | new_num = first_num * second_num 205 | return new_num 206 | 207 | 208 | def build_banner(): 209 | return html.Div( 210 | id='banner', 211 | className='banner', 212 | children=[ 213 | html.Img(src=app.get_asset_url('dsc-logo2.png')), 214 | ], 215 | ) 216 | 217 | 218 | # -------------------------- LOAD DATA ---------------------------- # 219 | 220 | 221 | csv_files_path = os.path.join('data/data.csv') 222 | 223 | data_df = pd.read_csv(csv_files_path) 224 | 225 | add_num_list = [] 226 | multiply_num_list = [] 227 | 228 | for index, row in data_df.iterrows(): 229 | add_num_list.append(add_numbers(row['first_num'], row['second_num'])) 230 | multiply_num_list.append(multiply_numbers(row['first_num'], row['second_num'])) 231 | 232 | data_df['add_num'] = add_num_list 233 | data_df['multiply_num'] = multiply_num_list 234 | 235 | 236 | # -------------------------- TEXT ---------------------------- # 237 | 238 | 239 | dash_text = ''' 240 | 241 | This is an example of a DSC dashboard. 242 | ''' 243 | 244 | 245 | # -------------------------- DASH ---------------------------- # 246 | 247 | 248 | external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] 249 | 250 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets, assets_folder='assets') 251 | server = app.server 252 | 253 | app.config.suppress_callback_exceptions = True 254 | 255 | 256 | # -------------------------- PROJECT DASHBOARD ---------------------------- # 257 | 258 | 259 | app.layout = html.Div(children=[ 260 | html.H1( 261 | children=[ 262 | build_banner(), 263 | html.P( 264 | id='instructions', 265 | children=dash_text), 266 | ] 267 | ), 268 | 269 | dcc.Graph( 270 | id='example-graph', 271 | figure={ 272 | 'data': [ 273 | {'x': data_df.index.values.tolist(), 'y': data_df['add_num'], 'type': 'bar', 'name': 'Add Numbers'}, 274 | {'x': data_df.index.values.tolist(), 'y': data_df['multiply_num'], 'type': 'bar', 'name': 'Multiply Numbers'}, 275 | ], 276 | 'layout': { 277 | 'title': 'Dash Data Visualization' 278 | } 279 | } 280 | ) 281 | ]) 282 | 283 | 284 | 285 | # -------------------------- MAIN ---------------------------- # 286 | 287 | 288 | if __name__ == '__main__': 289 | app.run_server(host='0.0.0.0', port=8080, debug=True, use_reloader=False) 290 | 291 | ``` 292 | 293 | ## Version control 294 | 295 | - v.1 of this blog post used a `flex` environment in the `app.yaml` file. However, this was causing the application to continuously run on GCP, causing increased costs. 296 | - v.2 is the current blog post, which uses a `standard` environment. 297 | -------------------------------------------------------------------------------- /simple-dash-app-engine-app/app.yaml: -------------------------------------------------------------------------------- 1 | service: simple-dash-app-engine-app 2 | runtime: python37 3 | 4 | basic_scaling: 5 | max_instances: 2 6 | idle_timeout: 10m 7 | 8 | resources: 9 | cpu: 1 10 | memory_gb: 1 11 | disk_size_gb: 10 12 | 13 | entrypoint: gunicorn -b 0.0.0.0:8080 main:server 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /simple-dash-app-engine-app/assets/base.css: -------------------------------------------------------------------------------- 1 | /* Table of contents 2 | –––––––––––––––––––––––––––––––––––––––––––––––––– 3 | - Plotly.js 4 | - Grid 5 | - Base Styles 6 | - Typography 7 | - Links 8 | - Buttons 9 | - Forms 10 | - Lists 11 | - Code 12 | - Tables 13 | - Spacing 14 | - Utilities 15 | - Clearing 16 | - Media Queries 17 | */ 18 | 19 | 20 | /* Grid 21 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 22 | .container { 23 | position: relative; 24 | width: 100%; 25 | max-width: 960px; 26 | margin: 0 auto; 27 | padding: 0 20px; 28 | box-sizing: border-box; } 29 | .column, 30 | .columns { 31 | width: 100%; 32 | float: left; 33 | box-sizing: border-box; } 34 | 35 | /* For devices larger than 400px */ 36 | @media (min-width: 400px) { 37 | .container { 38 | width: 85%; 39 | padding: 0; } 40 | } 41 | 42 | /* For devices larger than 550px */ 43 | @media (min-width: 550px) { 44 | .container { 45 | width: 80%; } 46 | .column, 47 | .columns { 48 | margin-left: 2%; } 49 | .column:first-child, 50 | .columns:first-child { 51 | margin-left: 1%; } 52 | 53 | .one.column, 54 | .one.columns { width: 4.66666666667%; } 55 | .two.columns { width: 13.3333333333%; } 56 | .three.columns { width: 22%; } 57 | .four.columns { width: 30.6666666667%; } 58 | .five.columns { width: 39.3333333333%; } 59 | .six.columns { width: 48%; } 60 | .seven.columns { width: 56.6666666667%; } 61 | .eight.columns { width: 65.3333333333%; } 62 | .nine.columns { width: 74.0%; } 63 | .ten.columns { width: 82.6666666667%; } 64 | .eleven.columns { width: 91.3333333333%; } 65 | .twelve.columns { width: 100%; margin-left: 0; } 66 | 67 | .one-third.column { width: 30.6666666667%; } 68 | .two-thirds.column { width: 65.3333333333%; } 69 | 70 | .one-half.column { width: 48%; } 71 | 72 | /* Offsets */ 73 | .offset-by-one.column, 74 | .offset-by-one.columns { margin-left: 8.66666666667%; } 75 | .offset-by-two.column, 76 | .offset-by-two.columns { margin-left: 17.3333333333%; } 77 | .offset-by-three.column, 78 | .offset-by-three.columns { margin-left: 26%; } 79 | .offset-by-four.column, 80 | .offset-by-four.columns { margin-left: 34.6666666667%; } 81 | .offset-by-five.column, 82 | .offset-by-five.columns { margin-left: 43.3333333333%; } 83 | .offset-by-six.column, 84 | .offset-by-six.columns { margin-left: 52%; } 85 | .offset-by-seven.column, 86 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 87 | .offset-by-eight.column, 88 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 89 | .offset-by-nine.column, 90 | .offset-by-nine.columns { margin-left: 78.0%; } 91 | .offset-by-ten.column, 92 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 93 | .offset-by-eleven.column, 94 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 95 | 96 | .offset-by-one-third.column, 97 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 98 | .offset-by-two-thirds.column, 99 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 100 | 101 | .offset-by-one-half.column, 102 | .offset-by-one-half.columns { margin-left: 52%; } 103 | 104 | } 105 | 106 | 107 | /* Base Styles 108 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 109 | /* NOTE 110 | html is set to 62.5% so that all the REM measurements throughout Skeleton 111 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 112 | html { 113 | font-size: 62.5%; } 114 | 115 | body { 116 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 117 | line-height: 1.6; 118 | font-weight: 400; 119 | font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 120 | color: rgb(50, 50, 50); 121 | margin: 0; 122 | } 123 | 124 | 125 | /* Typography 126 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 127 | h1, h2, h3, h4, h5, h6 { 128 | margin-top: 0; 129 | margin-bottom: 0; 130 | font-weight: 300; } 131 | h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } 132 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} 133 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} 134 | h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} 135 | h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} 136 | h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} 137 | 138 | p { 139 | margin-top: 0; } 140 | 141 | 142 | /* Blockquotes 143 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 144 | blockquote { 145 | border-left: 4px lightgrey solid; 146 | padding-left: 1rem; 147 | margin-top: 2rem; 148 | margin-bottom: 2rem; 149 | margin-left: 0rem; 150 | } 151 | 152 | 153 | /* Links 154 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 155 | a { 156 | color: #1EAEDB; 157 | text-decoration: underline; 158 | cursor: pointer;} 159 | a:hover { 160 | color: #0FA0CE; } 161 | 162 | 163 | /* Buttons 164 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 165 | .button, 166 | button, 167 | input[type="submit"], 168 | input[type="reset"], 169 | input[type="button"] { 170 | display: inline-block; 171 | height: 38px; 172 | padding: 0 30px; 173 | color: #555; 174 | text-align: center; 175 | font-size: 11px; 176 | font-weight: 600; 177 | line-height: 38px; 178 | letter-spacing: .1rem; 179 | text-transform: uppercase; 180 | text-decoration: none; 181 | white-space: nowrap; 182 | background-color: transparent; 183 | border-radius: 4px; 184 | border: 1px solid #bbb; 185 | cursor: pointer; 186 | box-sizing: border-box; } 187 | .button:hover, 188 | button:hover, 189 | input[type="submit"]:hover, 190 | input[type="reset"]:hover, 191 | input[type="button"]:hover, 192 | .button:focus, 193 | button:focus, 194 | input[type="submit"]:focus, 195 | input[type="reset"]:focus, 196 | input[type="button"]:focus { 197 | color: #333; 198 | border-color: #888; 199 | outline: 0; } 200 | .button.button-primary, 201 | button.button-primary, 202 | input[type="submit"].button-primary, 203 | input[type="reset"].button-primary, 204 | input[type="button"].button-primary { 205 | color: #FFF; 206 | background-color: #33C3F0; 207 | border-color: #33C3F0; } 208 | .button.button-primary:hover, 209 | button.button-primary:hover, 210 | input[type="submit"].button-primary:hover, 211 | input[type="reset"].button-primary:hover, 212 | input[type="button"].button-primary:hover, 213 | .button.button-primary:focus, 214 | button.button-primary:focus, 215 | input[type="submit"].button-primary:focus, 216 | input[type="reset"].button-primary:focus, 217 | input[type="button"].button-primary:focus { 218 | color: #FFF; 219 | background-color: #1EAEDB; 220 | border-color: #1EAEDB; } 221 | 222 | 223 | /* Forms 224 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 225 | input[type="email"], 226 | input[type="number"], 227 | input[type="search"], 228 | input[type="text"], 229 | input[type="tel"], 230 | input[type="url"], 231 | input[type="password"], 232 | textarea, 233 | select { 234 | height: 38px; 235 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 236 | background-color: #fff; 237 | border: 1px solid #D1D1D1; 238 | border-radius: 4px; 239 | box-shadow: none; 240 | box-sizing: border-box; 241 | font-family: inherit; 242 | font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} 243 | /* Removes awkward default styles on some inputs for iOS */ 244 | input[type="email"], 245 | input[type="number"], 246 | input[type="search"], 247 | input[type="text"], 248 | input[type="tel"], 249 | input[type="url"], 250 | input[type="password"], 251 | textarea { 252 | -webkit-appearance: none; 253 | -moz-appearance: none; 254 | appearance: none; } 255 | textarea { 256 | min-height: 65px; 257 | padding-top: 6px; 258 | padding-bottom: 6px; } 259 | input[type="email"]:focus, 260 | input[type="number"]:focus, 261 | input[type="search"]:focus, 262 | input[type="text"]:focus, 263 | input[type="tel"]:focus, 264 | input[type="url"]:focus, 265 | input[type="password"]:focus, 266 | textarea:focus, 267 | /*select:focus {*/ 268 | /* border: 1px solid #33C3F0;*/ 269 | /* outline: 0; }*/ 270 | label, 271 | legend { 272 | display: block; 273 | margin-bottom: 0px; } 274 | fieldset { 275 | padding: 0; 276 | border-width: 0; } 277 | input[type="checkbox"], 278 | input[type="radio"] { 279 | display: inline; } 280 | label > .label-body { 281 | display: inline-block; 282 | margin-left: .5rem; 283 | font-weight: normal; } 284 | 285 | 286 | /* Lists 287 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 288 | ul { 289 | list-style: circle inside; } 290 | ol { 291 | list-style: decimal inside; } 292 | ol, ul { 293 | padding-left: 0; 294 | margin-top: 0; } 295 | ul ul, 296 | ul ol, 297 | ol ol, 298 | ol ul { 299 | margin: 1.5rem 0 1.5rem 3rem; 300 | font-size: 90%; } 301 | li { 302 | margin-bottom: 1rem; } 303 | 304 | 305 | /* Tables 306 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 307 | table { 308 | border-collapse: collapse; 309 | } 310 | th, 311 | td { 312 | padding: 12px 15px; 313 | text-align: left; 314 | border-bottom: 1px solid #E1E1E1; } 315 | th:first-child, 316 | td:first-child { 317 | padding-left: 0; } 318 | th:last-child, 319 | td:last-child { 320 | padding-right: 0; } 321 | 322 | 323 | /* Spacing 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | button, 326 | .button { 327 | margin-bottom: 0rem; } 328 | input, 329 | textarea, 330 | select, 331 | fieldset { 332 | margin-bottom: 0rem; } 333 | pre, 334 | dl, 335 | figure, 336 | table, 337 | form { 338 | margin-bottom: 0rem; } 339 | p, 340 | ul, 341 | ol { 342 | margin-bottom: 0.75rem; } 343 | 344 | /* Utilities 345 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 346 | .u-full-width { 347 | width: 100%; 348 | box-sizing: border-box; } 349 | .u-max-full-width { 350 | max-width: 100%; 351 | box-sizing: border-box; } 352 | .u-pull-right { 353 | float: right; } 354 | .u-pull-left { 355 | float: left; } 356 | 357 | 358 | /* Misc 359 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 360 | hr { 361 | margin-top: 3rem; 362 | margin-bottom: 3.5rem; 363 | border-width: 0; 364 | border-top: 1px solid #E1E1E1; } 365 | 366 | 367 | /* Clearing 368 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 369 | 370 | /* Self Clearing Goodness */ 371 | .container:after, 372 | .row:after, 373 | .u-cf { 374 | content: ""; 375 | display: table; 376 | clear: both; } 377 | 378 | 379 | /* Media Queries 380 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 381 | /* 382 | Note: The best way to structure the use of media queries is to create the queries 383 | near the relevant code. For example, if you wanted to change the styles for buttons 384 | on small devices, paste the mobile query code up in the buttons section and style it 385 | there. 386 | */ 387 | 388 | 389 | /* Larger than mobile */ 390 | @media (min-width: 400px) {} 391 | 392 | /* Larger than phablet (also point when grid becomes active) */ 393 | @media (min-width: 550px) {} 394 | 395 | /* Larger than tablet */ 396 | @media (min-width: 750px) {} 397 | 398 | /* Larger than desktop */ 399 | @media (min-width: 1000px) {} 400 | 401 | /* Larger than Desktop HD */ 402 | @media (min-width: 1200px) {} 403 | -------------------------------------------------------------------------------- /simple-dash-app-engine-app/assets/dsc-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/simple-dash-app-engine-app/assets/dsc-logo2.png -------------------------------------------------------------------------------- /simple-dash-app-engine-app/assets/dsc.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,800'); 2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700'); 3 | 4 | .container.scalable { 5 | width: 95%; 6 | max-width: none; 7 | } 8 | 9 | @media print { 10 | body {-webkit-print-color-adjust: exact;} 11 | } 12 | 13 | /* Remove Undo 14 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 15 | ._dash-undo-redo { 16 | display: none; 17 | } 18 | 19 | 20 | @media screen { 21 | 22 | .banner Img { 23 | height: 5rem; 24 | margin-bottom: 1rem; 25 | } 26 | 27 | #instructions { 28 | font-size: 2rem; 29 | font-family: 'Open Sans', sans-serif; 30 | font-weight: 300; 31 | color: #001d4d; 32 | margin: 0rem 0rem 2rem ; 33 | } 34 | } 35 | 36 | @media print { 37 | 38 | #instructions { 39 | font-size: 2rem; 40 | font-family: 'Open Sans', sans-serif; 41 | font-weight: 300; 42 | color: #001d4d; 43 | margin: 0rem 0rem 2rem ; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /simple-dash-app-engine-app/data/data.csv: -------------------------------------------------------------------------------- 1 | ID,first_num,second_num 2 | 1,1,1 3 | 2,2,3 4 | 3,3,5 -------------------------------------------------------------------------------- /simple-dash-app-engine-app/main.py: -------------------------------------------------------------------------------- 1 | import dash 2 | import dash_core_components as dcc 3 | import dash_html_components as html 4 | import pandas as pd 5 | import os 6 | 7 | 8 | # -------------------------- PYTHON FUNCTIONS ---------------------------- # 9 | 10 | 11 | def add_numbers(first_num,second_num): 12 | new_num = first_num + second_num 13 | return new_num 14 | 15 | def multiply_numbers(first_num,second_num): 16 | new_num = first_num * second_num 17 | return new_num 18 | 19 | 20 | def build_banner(): 21 | return html.Div( 22 | id='banner', 23 | className='banner', 24 | children=[ 25 | html.Img(src=app.get_asset_url('dsc-logo2.png')), 26 | ], 27 | ) 28 | 29 | 30 | # -------------------------- LOAD DATA ---------------------------- # 31 | 32 | 33 | csv_files_path = os.path.join('data/data.csv') 34 | 35 | data_df = pd.read_csv(csv_files_path) 36 | 37 | add_num_list = [] 38 | multiply_num_list = [] 39 | 40 | for index, row in data_df.iterrows(): 41 | add_num_list.append(add_numbers(row['first_num'], row['second_num'])) 42 | multiply_num_list.append(multiply_numbers(row['first_num'], row['second_num'])) 43 | 44 | data_df['add_num'] = add_num_list 45 | data_df['multiply_num'] = multiply_num_list 46 | 47 | 48 | # -------------------------- TEXT ---------------------------- # 49 | 50 | 51 | dash_text = ''' 52 | 53 | This is an example of a DSC dashboard. 54 | ''' 55 | 56 | 57 | # -------------------------- DASH ---------------------------- # 58 | 59 | 60 | external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] 61 | 62 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets, assets_folder='assets') 63 | server = app.server 64 | 65 | app.config.suppress_callback_exceptions = True 66 | 67 | 68 | # -------------------------- PROJECT DASHBOARD ---------------------------- # 69 | 70 | 71 | app.layout = html.Div(children=[ 72 | html.H1( 73 | children=[ 74 | build_banner(), 75 | html.P( 76 | id='instructions', 77 | children=dash_text), 78 | ] 79 | ), 80 | 81 | dcc.Graph( 82 | id='example-graph', 83 | figure={ 84 | 'data': [ 85 | {'x': data_df.index.values.tolist(), 'y': data_df['add_num'], 'type': 'bar', 'name': 'Add Numbers'}, 86 | {'x': data_df.index.values.tolist(), 'y': data_df['multiply_num'], 'type': 'bar', 'name': 'Multiply Numbers'}, 87 | ], 88 | 'layout': { 89 | 'title': 'Dash Data Visualization' 90 | } 91 | } 92 | ) 93 | ]) 94 | 95 | 96 | 97 | # -------------------------- MAIN ---------------------------- # 98 | 99 | 100 | if __name__ == '__main__': 101 | app.run_server(host='0.0.0.0', port=8080, debug=True, use_reloader=False) -------------------------------------------------------------------------------- /simple-dash-app-engine-app/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | dash==1.6.0 3 | dash-core-components==1.5.0 4 | dash-html-components==1.0.1 5 | dash-renderer==1.2.0 6 | Flask==1.1.1 7 | Flask-Compress==1.4.0 8 | future==0.18.2 9 | itsdangerous==1.1.0 10 | Jinja2==2.10.3 11 | MarkupSafe==1.1.1 12 | numpy==1.16.5 13 | pandas==0.24.2 14 | pytz==2019.3 15 | retrying==1.3.3 16 | six==1.13.0 17 | Werkzeug==0.16.0 18 | gunicorn>=19.5.0 19 | google-api-core==1.16.0 20 | google-auth==1.10.1 21 | google-cloud-core==1.2.0 22 | google-cloud-storage==1.25.0 23 | google-resumable-media==0.5.0 24 | googleapis-common-protos==1.51.0 -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Python pycache: 17 | __pycache__/ 18 | # Ignored by the build system 19 | /setup.cfg 20 | 21 | # images 22 | /images/* 23 | 24 | # key 25 | data/key/* -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/README.md: -------------------------------------------------------------------------------- 1 | This guide builds on our previous post about [how to deploy your dash application with Google Cloud Platform's App Engine](https://datasciencecampus.github.io/deploy-dash-with-gcp/). This time we want to use Google Cloud's storage bucket to load data, so when we change the original data, the app updates automatically on a page refresh. 2 | 3 | ## Step 1: Making Your Dash App Load Data on Page Refresh 4 | 5 | In my first draft I skipped straight to uploading your data to GCP. However, this is the easy bit. The hardest bit of this pipeline is to make your Dash application refresh the input data on page refresh. There are multiple website suggesting that using global variables, as I did here, is not the way to use Dash. This is because changes to the data will be pushed to all users of the application. This is fine for updated, initial raw data, but very bad for if filtered data is pushed to all. 6 | 7 | Here, we only push changes to the initial, raw data. Therefore, we use global variables to solve this problem. 8 | 9 | The major changes to our application from previous guides comes in the form of two python functions: `data_in()` and `app_layout()`. The full script for `main.py` can be found at the bottom of this post. 10 | 11 | **data_in()** 12 | 13 | This function replaces the 'Load Data' section in the previous guides. We also introduce a better way of deciding if we want to load the data locally, or from the cloud (and then running locally, or on the cloud). It then provides the data pandas DataFrame as before. 14 | 15 | ``` 16 | def data_in(): 17 | 18 | if cloud == False: 19 | data = os.path.join('data/data.csv') 20 | 21 | else: 22 | project = 'dash-example-265811' 23 | project_name = 'dash-example-265811.appspot.com' 24 | folder_name = 'data' 25 | file_name = 'data.csv' 26 | 27 | if local == True: 28 | GCP = GCPDownloaderLocal() # run locally 29 | else: 30 | GCP = GCPDownloaderCloud() # run on cloud 31 | 32 | bytes_file = GCP.getData(project, project_name, folder_name, file_name) 33 | s = str(bytes_file, encoding='utf-8') 34 | data = StringIO(s) 35 | 36 | data_df = pd.read_csv(data) 37 | 38 | add_num_list = [] 39 | multiply_num_list = [] 40 | 41 | for index, row in data_df.iterrows(): 42 | add_num_list.append(add_numbers(row['first_num'], row['second_num'])) 43 | multiply_num_list.append(multiply_numbers(row['first_num'], row['second_num'])) 44 | 45 | data_df['add_num'] = add_num_list 46 | data_df['multiply_num'] = multiply_num_list 47 | 48 | return data_df 49 | 50 | ``` 51 | 52 | **app_layout()** 53 | 54 | This is the magic behind Dash's ability to update the site with your new data. In essence, we have just copied what was in the 'Project Dashboard' section previously, and called the `data_in()` function at the beginning. 55 | 56 | ``` 57 | def app_layout(): 58 | data_df = data_in() 59 | return html.Div(children=[ 60 | html.H1( 61 | children=[ 62 | build_banner(), 63 | html.P( 64 | id='instructions', 65 | children=dash_text), 66 | ] 67 | ), 68 | 69 | dcc.Graph( 70 | id='example-graph', 71 | figure={ 72 | 'data': [ 73 | {'x': data_df.index.values.tolist(), 'y': data_df['add_num'], 'type': 'bar', 'name': 'Add Numbers'}, 74 | {'x': data_df.index.values.tolist(), 'y': data_df['multiply_num'], 'type': 'bar', 'name': 'Multiply Numbers'}, 75 | ], 76 | 'layout': { 77 | 'title': 'Dash Data Visualization' 78 | } 79 | } 80 | ) 81 | ]) 82 | ``` 83 | 84 | **Other Updates** 85 | 86 | In the 'Dash' section four lines of code have been added. 87 | 88 | ``` 89 | local = False 90 | cloud = True 91 | 92 | data_df = data_in() 93 | 94 | app.layout = app_layout 95 | ``` 96 | 97 | The first two are just global variables specifying where you want to load and run the data. Then the data DataFrame is created initially before `app_layout` is run to update the application. Note, `app_layout` here should not have parentheses at the end. 98 | 99 | **Test and Update Data** 100 | 101 | To test your application, simply run `main.py`. The application should be available at [0.0.0.0:8080](0.0.0.0:8080). Next, alter the data in `data/data.csv`. Refresh [0.0.0.0:8080](0.0.0.0:8080) to see the graph change. 102 | 103 | ## Step 2: Upload Your Data to GCP 104 | 105 | Google's documentation provides a detailed guide on [how to upload objects](https://cloud.google.com/storage/docs/uploading-objects), here we simply this into the three main ways we use: 106 | 107 | **Using the GCP Online Browser** 108 | 109 | From Google's documentation: 110 | 111 | 1. Open the [Cloud Storage browser in the Google Cloud Console](https://console.cloud.google.com/storage/browser). 112 | 2. In the list of buckets, click on the name of the bucket that you want to upload an object to. You may want to create a separate folder within the bucket too. 113 | 3. In the Objects tab for the bucket, either: 114 | * Drag and drop the desired files from your desktop or file manager to the main pane in the Cloud Console. 115 | * Click the Upload Files button, select the files you want to upload in the dialog that appears, and click Open. 116 | 117 | **Using gsutil command line** 118 | 119 | This is probably the easiest method ([install gsutil guide](https://cloud.google.com/storage/docs/gsutil_install)). Simply type into your command line: 120 | 121 | ``` 122 | gsutil cp [OBJECT_LOCATION] gs://[DESTINATION_BUCKET_NAME]/ 123 | ``` 124 | 125 | **Using Python** 126 | 127 | In our [GitHub repository](https://github.com/datasciencecampus/deploy-dash-with-gcp) linked to this guide we provide a python file called `dataUpload.py` (code at the bottom of this post, also). This can be used to upload data to a specified bucket. However, this involves multiple additional steps to authorise your credentials - however, you will need to do this to download the data anyway! 128 | 129 | 1. Open the [Cloud Storage browser in the Google Cloud Console](https://console.cloud.google.com/storage/browser). 130 | 2. Hover over `APIs & Services` on the left hand side, and then click `Credentials` 131 | 3. Click `Create credentials`, then `Service account key`. 132 | ![Add credentials](../../images/training/dash/credentials.png) 133 | 4. Select `New service account`, then enter the service account name, change the role to Owner, and make sure the download is set to JSON. 134 | ![Add key](../../images/training/dash/new_key.png) 135 | 5. Save in a safe place (not `/Downloads`!) 136 | 137 | Lastly, you want to tell `dataUpload.py` where to look for the key. To avoid exposing key locations online, create a new file called `key_location.json` in `/data/keys` with the structure: 138 | 139 | 140 | ``` 141 | { 142 | "data": { 143 | "key_location": "/path/to/file.json" 144 | } 145 | } 146 | ``` 147 | 148 | Note, `key_location.json` will not be uploaded to GitHub or GCP as it is included in `.gitignore` and `.googleignore` files. However, if you delete or edit these, it may. 149 | 150 | ## Step 3: Link the Data to Your Dash App and Test Locally 151 | 152 | Before pushing the changes back to GCP. It is worth checking your data can be retrieved and loaded correctly. If you haven't downloaded the credential JSON key as shown above (and created the `key_location.json` file to show where the key can be found), please do so now. 153 | 154 | We can then link the data in `main.py` by changing the global variables '`local` and `cloud` to both be `True`. 155 | 156 | This will call the `GCPDownloaderLocal` class from the `dataDownloader.py` python script and obtain the data. This python script is available at our [GitHub repository](https://github.com/datasciencecampus/deploy-dash-with-gcp) and the bottom of this post. 157 | 158 | Your Dash app should be available as normal at [0.0.0.0:8000](0.0.0.0:8000). 159 | 160 | ## Step 4: Push the Changes to GCP 161 | 162 | Whereas when you run the code locally you require the JSON authentication, you do not when the data and dash application are part of the same project on Google Cloud Platform. 163 | 164 | In `main.py` change the global variable '`local` to be `False` and `cloud` to be `True`. This will call the `GCPDownloaderCloud` class in `dataDownloader.py`. 165 | 166 | This will call the `downloadDataCloud.py` python script. 167 | 168 | Next, push the changes to GCP using the gcloud command line tool. 169 | 170 | First, check your project is active in gcloud using: 171 | 172 | `gcloud config get-value project` 173 | 174 | Which will print the following on screen: 175 | 176 | ``` 177 | Your active configuration is: [default] 178 | 179 | my-project-id 180 | ``` 181 | 182 | To change the project to your desired project, type: 183 | 184 | `gcloud config set project project-id` 185 | 186 | Next, to deploy, type: 187 | 188 | `gcloud app deploy` 189 | 190 | Click `y` when prompted. Your app will be available at: 191 | 192 | `https://project-id.appspot.com/` 193 | 194 | The screenshot below shows the original data in `data.csv`. 195 | 196 | ![Our deployed dash app in GCP](../../images/training/dash/dash_app.png) 197 | 198 | The screenshot below shows the edited data in `data.csv`, which we pushed to the GCP bucket. 199 | 200 | ![Our updated deployed dash app in GCP](../../images/training/dash/dash_app_new.png) 201 | 202 | This app is running [here](https://simple-dash-app-with-a-bucket-dot-dash-example-265811.appspot.com). 203 | 204 | Visit our [GitHub repository](https://github.com/datasciencecampus/deploy-dash-with-gcp/tree/master/simple-dash-app-using-a-bucket) to view all the files. 205 | 206 | ## main.py script 207 | 208 | ``` 209 | import dash 210 | import dash_core_components as dcc 211 | import dash_html_components as html 212 | import pandas as pd 213 | from io import StringIO 214 | import os 215 | 216 | from data.dataDownloader import GCPDownloaderLocal, GCPDownloaderCloud 217 | 218 | # -------------------------- PYTHON FUNCTIONS ---------------------------- # 219 | 220 | 221 | def add_numbers(first_num,second_num): 222 | new_num = first_num + second_num 223 | return new_num 224 | 225 | def multiply_numbers(first_num,second_num): 226 | new_num = first_num * second_num 227 | return new_num 228 | 229 | 230 | def build_banner(): 231 | return html.Div( 232 | id='banner', 233 | className='banner', 234 | children=[ 235 | html.Img(src=app.get_asset_url('dsc-logo2.png')), 236 | ], 237 | ) 238 | 239 | def data_in(): 240 | 241 | if cloud == False: 242 | data = os.path.join('data/data.csv') 243 | 244 | else: 245 | project = 'dash-example-265811' 246 | project_name = 'dash-example-265811.appspot.com' 247 | folder_name = 'data' 248 | file_name = 'data.csv' 249 | 250 | if local == True: 251 | GCP = GCPDownloaderLocal() # run locally 252 | else: 253 | GCP = GCPDownloaderCloud() # run on cloud 254 | 255 | bytes_file = GCP.getData(project, project_name, folder_name, file_name) 256 | s = str(bytes_file, encoding='utf-8') 257 | data = StringIO(s) 258 | 259 | data_df = pd.read_csv(data) 260 | 261 | add_num_list = [] 262 | multiply_num_list = [] 263 | 264 | for index, row in data_df.iterrows(): 265 | add_num_list.append(add_numbers(row['first_num'], row['second_num'])) 266 | multiply_num_list.append(multiply_numbers(row['first_num'], row['second_num'])) 267 | 268 | data_df['add_num'] = add_num_list 269 | data_df['multiply_num'] = multiply_num_list 270 | 271 | return data_df 272 | 273 | def app_layout(): 274 | data_df = data_in() 275 | return html.Div(children=[ 276 | html.H1( 277 | children=[ 278 | build_banner(), 279 | html.P( 280 | id='instructions', 281 | children=dash_text), 282 | ] 283 | ), 284 | 285 | dcc.Graph( 286 | id='example-graph', 287 | figure={ 288 | 'data': [ 289 | {'x': data_df.index.values.tolist(), 'y': data_df['add_num'], 'type': 'bar', 'name': 'Add Numbers'}, 290 | {'x': data_df.index.values.tolist(), 'y': data_df['multiply_num'], 'type': 'bar', 'name': 'Multiply Numbers'}, 291 | ], 292 | 'layout': { 293 | 'title': 'Dash Data Visualization' 294 | } 295 | } 296 | ) 297 | ]) 298 | 299 | 300 | # -------------------------- TEXT ---------------------------- # 301 | 302 | 303 | dash_text = ''' 304 | 305 | This is an example of a DSC dashboard. 306 | ''' 307 | 308 | 309 | # -------------------------- DASH ---------------------------- # 310 | 311 | 312 | external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] 313 | 314 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets, assets_folder='assets') 315 | server = app.server 316 | 317 | local = False 318 | cloud = True 319 | 320 | data_df = data_in() 321 | 322 | app.layout = app_layout 323 | app.config.suppress_callback_exceptions = True 324 | 325 | 326 | # -------------------------- MAIN ---------------------------- # 327 | 328 | 329 | if __name__ == '__main__': 330 | app.run_server(host='0.0.0.0', port=8080, debug=True, use_reloader=False) 331 | 332 | ``` 333 | 334 | ## app.yaml files 335 | 336 | ``` 337 | service: default 338 | runtime: python37 339 | 340 | basic_scaling: 341 | max_instances: 2 342 | idle_timeout: 10m 343 | 344 | resources: 345 | cpu: 1 346 | memory_gb: 1 347 | disk_size_gb: 10 348 | 349 | entrypoint: gunicorn -b 0.0.0.0:8080 main:server 350 | ``` 351 | 352 | ## dataDownloader.py script 353 | 354 | ``` 355 | from google.cloud import storage 356 | from google.auth import compute_engine 357 | import os 358 | import json 359 | 360 | 361 | class GCPDownloaderLocal: 362 | def __init__(self): 363 | self.__key_location_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'key')) 364 | with open(self.__key_location_file + '/key_location.json', 'r') as json_file: 365 | self.__data = json.loads(json_file.read()) 366 | self.__key_path = self.__data['data']['key_location'] 367 | self.__storage_client = storage.Client.from_service_account_json(self.__key_path) 368 | 369 | def getData(self, project, project_name, folder_name, file_name): 370 | bucket = self.__storage_client.get_bucket(project_name) 371 | blob = bucket.blob(folder_name + '/' + file_name) 372 | content = blob.download_as_string() 373 | return content 374 | 375 | 376 | class GCPDownloaderCloud: 377 | 378 | def getData(self, project, project_name, folder_name, file_name): 379 | credentials = compute_engine.Credentials() 380 | storage_client = storage.Client(credentials=credentials, project=project) 381 | bucket = storage_client.get_bucket(project_name) 382 | blob = bucket.blob(folder_name + '/' + file_name) 383 | content = blob.download_as_string() 384 | return content 385 | ``` 386 | 387 | ## dataUpload.py script 388 | 389 | ``` 390 | from google.cloud import storage 391 | import os 392 | import json 393 | 394 | 395 | class GCPUploader: 396 | def __init__(self): 397 | self.__key_location_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'key')) 398 | with open(self.__key_location_file + '/key_location.json', 'r') as json_file: 399 | self.__data = json.loads(json_file.read()) 400 | self.__key_path = self.__data['data']['key_location'] 401 | self.__storage_client = storage.Client.from_service_account_json(self.__key_path) 402 | 403 | def upload_blob(self, project_name, df, destination_blob_name): 404 | """Uploads a file to the bucket.""" 405 | 406 | bucket = self.__storage_client.bucket(project_name) 407 | blob = bucket.blob(destination_blob_name) 408 | blob.upload_from_string(df.to_csv(), 'text/csv') 409 | 410 | ``` -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/simple-dash-app-using-a-bucket/__init__.py -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/app.yaml: -------------------------------------------------------------------------------- 1 | service: simple-dash-app-with-a-bucket 2 | runtime: python37 3 | 4 | basic_scaling: 5 | max_instances: 2 6 | idle_timeout: 10m 7 | 8 | resources: 9 | cpu: 1 10 | memory_gb: 1 11 | disk_size_gb: 10 12 | 13 | entrypoint: gunicorn -b 0.0.0.0:8080 main:server -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/assets/base.css: -------------------------------------------------------------------------------- 1 | /* Table of contents 2 | –––––––––––––––––––––––––––––––––––––––––––––––––– 3 | - Plotly.js 4 | - Grid 5 | - Base Styles 6 | - Typography 7 | - Links 8 | - Buttons 9 | - Forms 10 | - Lists 11 | - Code 12 | - Tables 13 | - Spacing 14 | - Utilities 15 | - Clearing 16 | - Media Queries 17 | */ 18 | 19 | 20 | /* Grid 21 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 22 | .container { 23 | position: relative; 24 | width: 100%; 25 | max-width: 960px; 26 | margin: 0 auto; 27 | padding: 0 20px; 28 | box-sizing: border-box; } 29 | .column, 30 | .columns { 31 | width: 100%; 32 | float: left; 33 | box-sizing: border-box; } 34 | 35 | /* For devices larger than 400px */ 36 | @media (min-width: 400px) { 37 | .container { 38 | width: 85%; 39 | padding: 0; } 40 | } 41 | 42 | /* For devices larger than 550px */ 43 | @media (min-width: 550px) { 44 | .container { 45 | width: 80%; } 46 | .column, 47 | .columns { 48 | margin-left: 2%; } 49 | .column:first-child, 50 | .columns:first-child { 51 | margin-left: 1%; } 52 | 53 | .one.column, 54 | .one.columns { width: 4.66666666667%; } 55 | .two.columns { width: 13.3333333333%; } 56 | .three.columns { width: 22%; } 57 | .four.columns { width: 30.6666666667%; } 58 | .five.columns { width: 39.3333333333%; } 59 | .six.columns { width: 48%; } 60 | .seven.columns { width: 56.6666666667%; } 61 | .eight.columns { width: 65.3333333333%; } 62 | .nine.columns { width: 74.0%; } 63 | .ten.columns { width: 82.6666666667%; } 64 | .eleven.columns { width: 91.3333333333%; } 65 | .twelve.columns { width: 100%; margin-left: 0; } 66 | 67 | .one-third.column { width: 30.6666666667%; } 68 | .two-thirds.column { width: 65.3333333333%; } 69 | 70 | .one-half.column { width: 48%; } 71 | 72 | /* Offsets */ 73 | .offset-by-one.column, 74 | .offset-by-one.columns { margin-left: 8.66666666667%; } 75 | .offset-by-two.column, 76 | .offset-by-two.columns { margin-left: 17.3333333333%; } 77 | .offset-by-three.column, 78 | .offset-by-three.columns { margin-left: 26%; } 79 | .offset-by-four.column, 80 | .offset-by-four.columns { margin-left: 34.6666666667%; } 81 | .offset-by-five.column, 82 | .offset-by-five.columns { margin-left: 43.3333333333%; } 83 | .offset-by-six.column, 84 | .offset-by-six.columns { margin-left: 52%; } 85 | .offset-by-seven.column, 86 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 87 | .offset-by-eight.column, 88 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 89 | .offset-by-nine.column, 90 | .offset-by-nine.columns { margin-left: 78.0%; } 91 | .offset-by-ten.column, 92 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 93 | .offset-by-eleven.column, 94 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 95 | 96 | .offset-by-one-third.column, 97 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 98 | .offset-by-two-thirds.column, 99 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 100 | 101 | .offset-by-one-half.column, 102 | .offset-by-one-half.columns { margin-left: 52%; } 103 | 104 | } 105 | 106 | 107 | /* Base Styles 108 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 109 | /* NOTE 110 | html is set to 62.5% so that all the REM measurements throughout Skeleton 111 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 112 | html { 113 | font-size: 62.5%; } 114 | 115 | body { 116 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 117 | line-height: 1.6; 118 | font-weight: 400; 119 | font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 120 | color: rgb(50, 50, 50); 121 | margin: 0; 122 | } 123 | 124 | 125 | /* Typography 126 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 127 | h1, h2, h3, h4, h5, h6 { 128 | margin-top: 0; 129 | margin-bottom: 0; 130 | font-weight: 300; } 131 | h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } 132 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} 133 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} 134 | h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} 135 | h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} 136 | h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} 137 | 138 | p { 139 | margin-top: 0; } 140 | 141 | 142 | /* Blockquotes 143 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 144 | blockquote { 145 | border-left: 4px lightgrey solid; 146 | padding-left: 1rem; 147 | margin-top: 2rem; 148 | margin-bottom: 2rem; 149 | margin-left: 0rem; 150 | } 151 | 152 | 153 | /* Links 154 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 155 | a { 156 | color: #1EAEDB; 157 | text-decoration: underline; 158 | cursor: pointer;} 159 | a:hover { 160 | color: #0FA0CE; } 161 | 162 | 163 | /* Buttons 164 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 165 | .button, 166 | button, 167 | input[type="submit"], 168 | input[type="reset"], 169 | input[type="button"] { 170 | display: inline-block; 171 | height: 38px; 172 | padding: 0 30px; 173 | color: #555; 174 | text-align: center; 175 | font-size: 11px; 176 | font-weight: 600; 177 | line-height: 38px; 178 | letter-spacing: .1rem; 179 | text-transform: uppercase; 180 | text-decoration: none; 181 | white-space: nowrap; 182 | background-color: transparent; 183 | border-radius: 4px; 184 | border: 1px solid #bbb; 185 | cursor: pointer; 186 | box-sizing: border-box; } 187 | .button:hover, 188 | button:hover, 189 | input[type="submit"]:hover, 190 | input[type="reset"]:hover, 191 | input[type="button"]:hover, 192 | .button:focus, 193 | button:focus, 194 | input[type="submit"]:focus, 195 | input[type="reset"]:focus, 196 | input[type="button"]:focus { 197 | color: #333; 198 | border-color: #888; 199 | outline: 0; } 200 | .button.button-primary, 201 | button.button-primary, 202 | input[type="submit"].button-primary, 203 | input[type="reset"].button-primary, 204 | input[type="button"].button-primary { 205 | color: #FFF; 206 | background-color: #33C3F0; 207 | border-color: #33C3F0; } 208 | .button.button-primary:hover, 209 | button.button-primary:hover, 210 | input[type="submit"].button-primary:hover, 211 | input[type="reset"].button-primary:hover, 212 | input[type="button"].button-primary:hover, 213 | .button.button-primary:focus, 214 | button.button-primary:focus, 215 | input[type="submit"].button-primary:focus, 216 | input[type="reset"].button-primary:focus, 217 | input[type="button"].button-primary:focus { 218 | color: #FFF; 219 | background-color: #1EAEDB; 220 | border-color: #1EAEDB; } 221 | 222 | 223 | /* Forms 224 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 225 | input[type="email"], 226 | input[type="number"], 227 | input[type="search"], 228 | input[type="text"], 229 | input[type="tel"], 230 | input[type="url"], 231 | input[type="password"], 232 | textarea, 233 | select { 234 | height: 38px; 235 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 236 | background-color: #fff; 237 | border: 1px solid #D1D1D1; 238 | border-radius: 4px; 239 | box-shadow: none; 240 | box-sizing: border-box; 241 | font-family: inherit; 242 | font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} 243 | /* Removes awkward default styles on some inputs for iOS */ 244 | input[type="email"], 245 | input[type="number"], 246 | input[type="search"], 247 | input[type="text"], 248 | input[type="tel"], 249 | input[type="url"], 250 | input[type="password"], 251 | textarea { 252 | -webkit-appearance: none; 253 | -moz-appearance: none; 254 | appearance: none; } 255 | textarea { 256 | min-height: 65px; 257 | padding-top: 6px; 258 | padding-bottom: 6px; } 259 | input[type="email"]:focus, 260 | input[type="number"]:focus, 261 | input[type="search"]:focus, 262 | input[type="text"]:focus, 263 | input[type="tel"]:focus, 264 | input[type="url"]:focus, 265 | input[type="password"]:focus, 266 | textarea:focus, 267 | /*select:focus {*/ 268 | /* border: 1px solid #33C3F0;*/ 269 | /* outline: 0; }*/ 270 | label, 271 | legend { 272 | display: block; 273 | margin-bottom: 0px; } 274 | fieldset { 275 | padding: 0; 276 | border-width: 0; } 277 | input[type="checkbox"], 278 | input[type="radio"] { 279 | display: inline; } 280 | label > .label-body { 281 | display: inline-block; 282 | margin-left: .5rem; 283 | font-weight: normal; } 284 | 285 | 286 | /* Lists 287 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 288 | ul { 289 | list-style: circle inside; } 290 | ol { 291 | list-style: decimal inside; } 292 | ol, ul { 293 | padding-left: 0; 294 | margin-top: 0; } 295 | ul ul, 296 | ul ol, 297 | ol ol, 298 | ol ul { 299 | margin: 1.5rem 0 1.5rem 3rem; 300 | font-size: 90%; } 301 | li { 302 | margin-bottom: 1rem; } 303 | 304 | 305 | /* Tables 306 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 307 | table { 308 | border-collapse: collapse; 309 | } 310 | th, 311 | td { 312 | padding: 12px 15px; 313 | text-align: left; 314 | border-bottom: 1px solid #E1E1E1; } 315 | th:first-child, 316 | td:first-child { 317 | padding-left: 0; } 318 | th:last-child, 319 | td:last-child { 320 | padding-right: 0; } 321 | 322 | 323 | /* Spacing 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | button, 326 | .button { 327 | margin-bottom: 0rem; } 328 | input, 329 | textarea, 330 | select, 331 | fieldset { 332 | margin-bottom: 0rem; } 333 | pre, 334 | dl, 335 | figure, 336 | table, 337 | form { 338 | margin-bottom: 0rem; } 339 | p, 340 | ul, 341 | ol { 342 | margin-bottom: 0.75rem; } 343 | 344 | /* Utilities 345 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 346 | .u-full-width { 347 | width: 100%; 348 | box-sizing: border-box; } 349 | .u-max-full-width { 350 | max-width: 100%; 351 | box-sizing: border-box; } 352 | .u-pull-right { 353 | float: right; } 354 | .u-pull-left { 355 | float: left; } 356 | 357 | 358 | /* Misc 359 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 360 | hr { 361 | margin-top: 3rem; 362 | margin-bottom: 3.5rem; 363 | border-width: 0; 364 | border-top: 1px solid #E1E1E1; } 365 | 366 | 367 | /* Clearing 368 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 369 | 370 | /* Self Clearing Goodness */ 371 | .container:after, 372 | .row:after, 373 | .u-cf { 374 | content: ""; 375 | display: table; 376 | clear: both; } 377 | 378 | 379 | /* Media Queries 380 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 381 | /* 382 | Note: The best way to structure the use of media queries is to create the queries 383 | near the relevant code. For example, if you wanted to change the styles for buttons 384 | on small devices, paste the mobile query code up in the buttons section and style it 385 | there. 386 | */ 387 | 388 | 389 | /* Larger than mobile */ 390 | @media (min-width: 400px) {} 391 | 392 | /* Larger than phablet (also point when grid becomes active) */ 393 | @media (min-width: 550px) {} 394 | 395 | /* Larger than tablet */ 396 | @media (min-width: 750px) {} 397 | 398 | /* Larger than desktop */ 399 | @media (min-width: 1000px) {} 400 | 401 | /* Larger than Desktop HD */ 402 | @media (min-width: 1200px) {} 403 | -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/assets/dsc-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/simple-dash-app-using-a-bucket/assets/dsc-logo2.png -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/assets/dsc.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,800'); 2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700'); 3 | 4 | .container.scalable { 5 | width: 95%; 6 | max-width: none; 7 | } 8 | 9 | @media print { 10 | body {-webkit-print-color-adjust: exact;} 11 | } 12 | 13 | /* Remove Undo 14 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 15 | ._dash-undo-redo { 16 | display: none; 17 | } 18 | 19 | 20 | @media screen { 21 | 22 | .banner Img { 23 | height: 5rem; 24 | margin-bottom: 1rem; 25 | } 26 | 27 | #instructions { 28 | font-size: 2rem; 29 | font-family: 'Open Sans', sans-serif; 30 | font-weight: 300; 31 | color: #001d4d; 32 | margin: 0rem 0rem 2rem ; 33 | } 34 | } 35 | 36 | @media print { 37 | 38 | #instructions { 39 | font-size: 2rem; 40 | font-family: 'Open Sans', sans-serif; 41 | font-weight: 300; 42 | color: #001d4d; 43 | margin: 0rem 0rem 2rem ; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datasciencecampus/deploy-dash-with-gcp/eea72bf55e3a574f869bfa6c0e67a4a76d1bdfa7/simple-dash-app-using-a-bucket/data/__init__.py -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/data/data.csv: -------------------------------------------------------------------------------- 1 | ID,first_num,second_num 2 | 1,1,1 3 | 2,2,3 4 | 3,3,5 -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/data/dataDownloader.py: -------------------------------------------------------------------------------- 1 | from google.cloud import storage 2 | from google.auth import compute_engine 3 | import os 4 | import json 5 | 6 | 7 | class GCPDownloaderLocal: 8 | def __init__(self): 9 | self.__key_location_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'key')) 10 | with open(self.__key_location_file + '/key_location.json', 'r') as json_file: 11 | self.__data = json.loads(json_file.read()) 12 | self.__key_path = self.__data['data']['key_location'] 13 | self.__storage_client = storage.Client.from_service_account_json(self.__key_path) 14 | 15 | def getData(self, project, project_name, folder_name, file_name): 16 | bucket = self.__storage_client.get_bucket(project_name) 17 | blob = bucket.blob(folder_name + '/' + file_name) 18 | content = blob.download_as_string() 19 | return content 20 | 21 | 22 | class GCPDownloaderCloud: 23 | 24 | def getData(self, project, project_name, folder_name, file_name): 25 | credentials = compute_engine.Credentials() 26 | storage_client = storage.Client(credentials=credentials, project=project) 27 | bucket = storage_client.get_bucket(project_name) 28 | blob = bucket.blob(folder_name + '/' + file_name) 29 | content = blob.download_as_string() 30 | return content 31 | 32 | 33 | if __name__ == '__main__': 34 | project_name = 'dash-example-265811.appspot.com' 35 | folder_name = 'data' 36 | file_name = 'data.csv' 37 | GCP = GCPDownloaderLocal() 38 | content = GCP.getData(project_name, folder_name, file_name) -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/data/dataUpload.py: -------------------------------------------------------------------------------- 1 | from google.cloud import storage 2 | import os 3 | import json 4 | 5 | 6 | class GCPUploader: 7 | def __init__(self): 8 | self.__key_location_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'key')) 9 | with open(self.__key_location_file + '/key_location.json', 'r') as json_file: 10 | self.__data = json.loads(json_file.read()) 11 | self.__key_path = self.__data['data']['key_location'] 12 | self.__storage_client = storage.Client.from_service_account_json(self.__key_path) 13 | 14 | def upload_blob(self, project_name, df, destination_blob_name): 15 | """Uploads a file to the bucket.""" 16 | 17 | bucket = self.__storage_client.bucket(project_name) 18 | blob = bucket.blob(destination_blob_name) 19 | blob.upload_from_string(df.to_csv(), 'text/csv') 20 | 21 | 22 | if __name__ == '__main__': 23 | 24 | df = create_data_frame() 25 | project_name = 26 | destination_blob_name = 27 | upload = GCPUploader() 28 | upload.upload_blob(project_name, df, destination_blob_name) -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/main.py: -------------------------------------------------------------------------------- 1 | import dash 2 | import dash_core_components as dcc 3 | import dash_html_components as html 4 | import pandas as pd 5 | from io import StringIO 6 | import os 7 | 8 | from data.dataDownloader import GCPDownloaderLocal, GCPDownloaderCloud 9 | 10 | # -------------------------- PYTHON FUNCTIONS ---------------------------- # 11 | 12 | 13 | def add_numbers(first_num,second_num): 14 | new_num = first_num + second_num 15 | return new_num 16 | 17 | def multiply_numbers(first_num,second_num): 18 | new_num = first_num * second_num 19 | return new_num 20 | 21 | 22 | def build_banner(): 23 | return html.Div( 24 | id='banner', 25 | className='banner', 26 | children=[ 27 | html.Img(src=app.get_asset_url('dsc-logo2.png')), 28 | ], 29 | ) 30 | 31 | def data_in(): 32 | 33 | if cloud == False: 34 | data = os.path.join('data/data.csv') 35 | 36 | else: 37 | project = 'dash-example-265811' 38 | project_name = 'dash-example-265811.appspot.com' 39 | folder_name = 'data' 40 | file_name = 'data.csv' 41 | 42 | if local == True: 43 | GCP = GCPDownloaderLocal() # run locally 44 | else: 45 | GCP = GCPDownloaderCloud() # run on cloud 46 | 47 | bytes_file = GCP.getData(project, project_name, folder_name, file_name) 48 | s = str(bytes_file, encoding='utf-8') 49 | data = StringIO(s) 50 | 51 | data_df = pd.read_csv(data) 52 | 53 | add_num_list = [] 54 | multiply_num_list = [] 55 | 56 | for index, row in data_df.iterrows(): 57 | add_num_list.append(add_numbers(row['first_num'], row['second_num'])) 58 | multiply_num_list.append(multiply_numbers(row['first_num'], row['second_num'])) 59 | 60 | data_df['add_num'] = add_num_list 61 | data_df['multiply_num'] = multiply_num_list 62 | 63 | return data_df 64 | 65 | def app_layout(): 66 | data_df = data_in() 67 | return html.Div(children=[ 68 | html.H1( 69 | children=[ 70 | build_banner(), 71 | html.P( 72 | id='instructions', 73 | children=dash_text), 74 | ] 75 | ), 76 | 77 | dcc.Graph( 78 | id='example-graph', 79 | figure={ 80 | 'data': [ 81 | {'x': data_df.index.values.tolist(), 'y': data_df['add_num'], 'type': 'bar', 'name': 'Add Numbers'}, 82 | {'x': data_df.index.values.tolist(), 'y': data_df['multiply_num'], 'type': 'bar', 'name': 'Multiply Numbers'}, 83 | ], 84 | 'layout': { 85 | 'title': 'Dash Data Visualization' 86 | } 87 | } 88 | ) 89 | ]) 90 | 91 | 92 | # -------------------------- TEXT ---------------------------- # 93 | 94 | 95 | dash_text = ''' 96 | 97 | This is an example of a DSC dashboard. 98 | ''' 99 | 100 | 101 | # -------------------------- DASH ---------------------------- # 102 | 103 | 104 | external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] 105 | 106 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets, assets_folder='assets') 107 | server = app.server 108 | 109 | local = False 110 | cloud = True 111 | 112 | data_df = data_in() 113 | 114 | app.layout = app_layout 115 | app.config.suppress_callback_exceptions = True 116 | 117 | 118 | # -------------------------- MAIN ---------------------------- # 119 | 120 | 121 | if __name__ == '__main__': 122 | app.run_server(host='0.0.0.0', port=8080, debug=True, use_reloader=False) -------------------------------------------------------------------------------- /simple-dash-app-using-a-bucket/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | dash==1.6.0 3 | dash-core-components==1.5.0 4 | dash-html-components==1.0.1 5 | dash-renderer==1.2.0 6 | Flask==1.1.1 7 | Flask-Compress==1.4.0 8 | future==0.18.2 9 | itsdangerous==1.1.0 10 | Jinja2==2.10.3 11 | MarkupSafe==1.1.1 12 | numpy==1.16.5 13 | pandas==0.24.2 14 | pytz==2019.3 15 | retrying==1.3.3 16 | six==1.13.0 17 | Werkzeug==0.16.0 18 | gunicorn>=19.5.0 19 | google-api-core==1.16.0 20 | google-auth==1.10.1 21 | google-cloud-core==1.2.0 22 | google-cloud-storage==1.25.0 23 | google-resumable-media==0.5.0 24 | googleapis-common-protos==1.51.0 --------------------------------------------------------------------------------