├── src ├── __init__.py ├── pages │ ├── complex_page │ │ ├── __init__.py │ │ ├── comp1.py │ │ └── layout.py │ ├── __init__.py │ ├── logout.py │ ├── login.py │ ├── not_found_404.py │ ├── home.py │ └── page2.py ├── gunicorn_config.py ├── assets │ ├── favicon.ico │ └── logos │ │ ├── logo_main.png │ │ └── logo_small.png ├── .env.development ├── components │ ├── __init__.py │ ├── footer.py │ ├── dog_image.py │ ├── number_fact_aio.py │ ├── navbar.py │ └── login.py ├── utils │ ├── __init__.py │ ├── settings.py │ ├── images.py │ └── api.py └── app.py ├── tests ├── __init__.py └── pages │ ├── __init__.py │ └── test_home_callbacks.py ├── Procfile ├── .gitattributes ├── requirements.txt ├── Dockerfile ├── LICENSE ├── .gitignore └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/complex_page/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn --timeout 600 --chdir src app:server 2 | -------------------------------------------------------------------------------- /src/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | bind = '0.0.0.0:8085' 2 | workers = 2 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradley-erickson/dash-app-structure/HEAD/requirements.txt -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradley-erickson/dash-app-structure/HEAD/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/logos/logo_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradley-erickson/dash-app-structure/HEAD/src/assets/logos/logo_main.png -------------------------------------------------------------------------------- /src/assets/logos/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradley-erickson/dash-app-structure/HEAD/src/assets/logos/logo_small.png -------------------------------------------------------------------------------- /src/.env.development: -------------------------------------------------------------------------------- 1 | HOST=127.0.0.1 2 | PORT=8085 3 | DEBUG=True 4 | DEV_TOOLS_PROPS_CHECK=True 5 | API_KEY=secretapikey 6 | SECRET_KEY=supersecret 7 | -------------------------------------------------------------------------------- /src/components/__init__.py: -------------------------------------------------------------------------------- 1 | # pull in components from files in the current directory to make imports cleaner 2 | from .navbar import navbar 3 | from .footer import footer 4 | from .dog_image import create_dog_image_card 5 | from .number_fact_aio import NumberFactAIO 6 | -------------------------------------------------------------------------------- /tests/pages/test_home_callbacks.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from src.pages.home import home_radios 3 | 4 | # CURRENTLY FAILS BECAUSE OF dash.register_page 5 | def test_home_radio_callback(): 6 | value = 5 7 | output = home_radios(value) 8 | assert output == f'You have selected {value}' 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | COPY requirements.txt / 4 | 5 | RUN pip install -r /requirements.txt \ 6 | && rm -rf /root/.cache 7 | 8 | COPY src/ ./ 9 | 10 | ENV ENVIRONMENT_FILE=".env" 11 | 12 | EXPOSE 8085 13 | 14 | ENTRYPOINT ["gunicorn", "--config", "gunicorn_config.py", "app:server"] 15 | -------------------------------------------------------------------------------- /src/pages/__init__.py: -------------------------------------------------------------------------------- 1 | # on the chance that we need to import something from this directory 2 | # we need this empty __init__.py file 3 | # If you are curious about the function of the __init__.py file, check out the official python documentation 4 | # https://docs.python.org/3/reference/import.html#regular-packages 5 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # on the chance that we need to import something from this directory 2 | # we need this empty __init__.py file 3 | # If you are curious about the function of the __init__.py file, check out the official python documentation 4 | # https://docs.python.org/3/reference/import.html#regular-packages 5 | -------------------------------------------------------------------------------- /src/pages/complex_page/comp1.py: -------------------------------------------------------------------------------- 1 | #notes 2 | ''' 3 | If a page has a lot of components, I suggest splitting them up into their own files such as this one. 4 | ''' 5 | 6 | # package imports 7 | from dash import html 8 | 9 | random_component = html.Div('This is a random component specific to the current page.') 10 | -------------------------------------------------------------------------------- /src/pages/logout.py: -------------------------------------------------------------------------------- 1 | # package imports 2 | import dash 3 | from dash import html, dcc 4 | 5 | dash.register_page(__name__) 6 | 7 | layout = html.Div( 8 | [ 9 | html.Div(html.H2('You have been logged out - Please login')), 10 | html.Br(), 11 | dcc.Link('Home', href='/'), 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /src/pages/login.py: -------------------------------------------------------------------------------- 1 | # package imports 2 | import dash 3 | import dash_bootstrap_components as dbc 4 | 5 | # local imports 6 | from components.login import login_card 7 | 8 | dash.register_page(__name__) 9 | 10 | # login screen 11 | layout = dbc.Row( 12 | dbc.Col( 13 | login_card, 14 | md=6, 15 | lg=4, 16 | xxl=3, 17 | ), 18 | justify='center' 19 | ) 20 | -------------------------------------------------------------------------------- /src/utils/settings.py: -------------------------------------------------------------------------------- 1 | # package imports 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | cwd = os.getcwd() 6 | dotenv_path = os.path.join(cwd, os.getenv('ENVIRONMENT_FILE', '.env.development')) 7 | load_dotenv(dotenv_path=dotenv_path, override=True) 8 | 9 | APP_HOST = os.environ.get('HOST') 10 | APP_PORT = int(os.environ.get('PORT')) 11 | APP_DEBUG = bool(os.environ.get('DEBUG')) 12 | DEV_TOOLS_PROPS_CHECK = bool(os.environ.get('DEV_TOOLS_PROPS_CHECK')) 13 | API_KEY = os.environ.get('API_KEY', None) 14 | -------------------------------------------------------------------------------- /src/pages/not_found_404.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file creates the 404 not found page. 4 | If this file is not included, Dash will the same layout shown below. 5 | If you need a more customized 404 not found page, modify this file. 6 | ''' 7 | 8 | # package imports 9 | import dash 10 | from dash import html 11 | 12 | dash.register_page(__name__, path='/404') 13 | 14 | layout = html.Div( 15 | [ 16 | html.H1('404 - Page not found'), 17 | html.Div( 18 | html.A('Return home', href='/') 19 | ) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /src/components/footer.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file is for creating a simple footer element. 4 | This component will sit at the bottom of each page of the application. 5 | ''' 6 | 7 | # package imports 8 | from dash import html 9 | import dash_bootstrap_components as dbc 10 | 11 | footer = html.Footer( 12 | dbc.Container( 13 | [ 14 | html.Hr(), 15 | 'Footer item 1', 16 | html.Br(), 17 | 'Footer item 2', 18 | html.Br(), 19 | 'Footer item 3' 20 | ] 21 | ) 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/dog_image.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file creates a simples dog image card. 4 | This is useful for when you are trying to keep your styling really consistent across multiple items. 5 | For instance, our card is outline with the warning color. 6 | ''' 7 | 8 | # package imports 9 | import dash_bootstrap_components as dbc 10 | 11 | # local imports 12 | from utils.images import get_dog_image 13 | 14 | def create_dog_image_card(breed, name): 15 | src = get_dog_image(breed, name) 16 | card = dbc.Card( 17 | [ 18 | dbc.CardHeader(f'Dog {breed} {name}'), 19 | dbc.CardImg(src=src) 20 | ], 21 | class_name='w-50', 22 | color='warning', 23 | outline=True 24 | ) 25 | return card 26 | -------------------------------------------------------------------------------- /src/pages/home.py: -------------------------------------------------------------------------------- 1 | # package imports 2 | import dash 3 | from dash import html, dcc, callback, Input, Output 4 | 5 | dash.register_page( 6 | __name__, 7 | path='/', 8 | redirect_from=['/home'], 9 | title='Home' 10 | ) 11 | 12 | layout = html.Div( 13 | [ 14 | html.H1('Home page!'), 15 | html.Div( 16 | html.A('Checkout the complex page here.', href='/complex') 17 | ), 18 | html.A('/page2', href='/page2'), 19 | dcc.RadioItems( 20 | id='radios', 21 | options=[{'label': i, 'value': i} for i in ['Orange', 'Blue', 'Red']], 22 | value='Orange', 23 | ), 24 | html.Div(id='content') 25 | ] 26 | ) 27 | 28 | @callback(Output('content', 'children'), Input('radios', 'value')) 29 | def home_radios(value): 30 | return f'You have selected {value}' 31 | -------------------------------------------------------------------------------- /src/utils/images.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file is used for handling anything image related. 4 | I suggest handling the local file encoding/decoding here as well as fetching any external images. 5 | ''' 6 | 7 | # package imports 8 | import base64 9 | import os 10 | 11 | # image CDNs 12 | image_cdn = 'https://images.dog.ceo/breeds' 13 | 14 | # logo information 15 | cwd = os.getcwd() 16 | logo_path = os.path.join(cwd, 'assets', 'logos', 'logo_main.png') 17 | logo_tunel = base64.b64encode(open(logo_path, 'rb').read()) 18 | logo_encoded = 'data:image/png;base64,{}'.format(logo_tunel.decode()) 19 | 20 | 21 | def get_dog_image(breed, name): 22 | ''' 23 | This method assumes that you are fetching specific images hosted on a CDN. 24 | For instance, random dog pics given a breed. 25 | ''' 26 | if breed and name: 27 | return f'{image_cdn}/{breed}/{name}.jpg' 28 | return None 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Brad Erickson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pages/complex_page/layout.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This directory is meant to be for a specific page. 4 | We will define the page and import any page specific components that we define in this directory. 5 | This file should serve the layouts and callbacks. 6 | The callbacks could be in their own file, but you'll need to make sure to import the file so they load. 7 | ''' 8 | 9 | # package imports 10 | import dash 11 | from dash import html 12 | import dash_bootstrap_components as dbc 13 | 14 | # local imports 15 | from .comp1 import random_component 16 | from components import create_dog_image_card, NumberFactAIO 17 | 18 | dash.register_page( 19 | __name__, 20 | path='/complex', 21 | title='Complex page' 22 | ) 23 | 24 | layout = html.Div( 25 | [ 26 | html.H3('Random component'), 27 | random_component, 28 | html.H3('Dog pics'), 29 | dbc.Row( 30 | [ 31 | create_dog_image_card('labrador', 'n02099712_3503'), 32 | create_dog_image_card('labrador', 'n02099712_607') 33 | ] 34 | ), 35 | html.H3('Number Fact'), 36 | NumberFactAIO(number=1) 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /src/utils/api.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file is for creating a requests session so you can securely load in your api key. 4 | We then create a session and add the api key to the header. 5 | Depending on the API you are using, you might need to modify the value `x-api-key`. 6 | ''' 7 | 8 | # package imports 9 | import os 10 | import requests 11 | 12 | # local imports 13 | from .settings import API_KEY 14 | 15 | # this is an example API url that produces a fact about a number 16 | api_url = 'http://numbersapi.com' 17 | header_key = {'x-api-key': API_KEY} 18 | 19 | # create a session and update the headers 20 | # this API does not require any authentication, so we don't need to update the headers 21 | session = requests.Session() 22 | session.headers.update(header_key) 23 | 24 | 25 | def get_number_fact(number): 26 | '''Format the proper url, then call for a fact about the number''' 27 | url = f'{api_url}/{number}/trivia' 28 | r = session.get(url) 29 | 30 | # if the response is not 200, then something went wrong and we should return an emtpy string 31 | if r.status_code != 200: 32 | return '' 33 | else: 34 | return r.content.decode('utf-8') 35 | -------------------------------------------------------------------------------- /src/pages/page2.py: -------------------------------------------------------------------------------- 1 | # package imports 2 | import dash 3 | from dash import html, dcc, Output, Input, callback 4 | import dash_bootstrap_components as dbc 5 | from flask_login import current_user 6 | 7 | dash.register_page(__name__) 8 | 9 | logged_out_layout = html.Div( 10 | [ 11 | 'Please login before viewing this page.', 12 | html.Br(), 13 | dbc.Button( 14 | 'Login', 15 | href='/login' 16 | ) 17 | ], 18 | className='text-center' 19 | ) 20 | 21 | logged_in_layout = html.Div( 22 | [ 23 | html.H1('Page 2'), 24 | dcc.RadioItems( 25 | id='page-2-radios', 26 | options=[{'label': i, 'value': i} for i in ['Orange', 'Blue', 'Red']], 27 | value='Orange', 28 | ), 29 | html.Div(id='page-2-content'), 30 | html.Br(), 31 | dcc.Link('Go to Page 1', href='/page-1'), 32 | html.Br(), 33 | dcc.Link('Go back to home', href='/'), 34 | ] 35 | ) 36 | 37 | def layout(): 38 | if not current_user.is_authenticated: 39 | return logged_out_layout 40 | return logged_in_layout 41 | 42 | 43 | @callback(Output('page-2-content', 'children'), Input('page-2-radios', 'value')) 44 | def page_2_radios(value): 45 | return f'You have selected {value}' 46 | -------------------------------------------------------------------------------- /src/components/number_fact_aio.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file is a simple AIO component that contains an input and a div. 4 | The input determines which number you want a fact of. 5 | For more information about AIO components, check out the official documentation: 6 | https://dash.plotly.com/all-in-one-components 7 | ''' 8 | 9 | # package imports 10 | from dash import html, dcc, callback, Output, Input, MATCH 11 | import dash_bootstrap_components as dbc 12 | import uuid 13 | 14 | # local imports 15 | from utils.api import get_number_fact 16 | 17 | 18 | class NumberFactAIO(html.Div): 19 | 20 | class ids: 21 | text = lambda aio_id: { 22 | 'component': 'NumberFactAIO', 23 | 'subcomponent': 'div', 24 | 'aio_id': aio_id 25 | } 26 | input = lambda aio_id: { 27 | 'component': 'NumberFactAIO', 28 | 'subcomponent': 'input', 29 | 'aio_id': aio_id 30 | } 31 | 32 | ids = ids 33 | 34 | def __init__( 35 | self, 36 | number=0, 37 | aio_id=None 38 | ): 39 | if aio_id is None: 40 | aio_id = str(uuid.uuid4()) 41 | 42 | fact = get_number_fact(number) 43 | 44 | super().__init__([ 45 | dcc.Input( 46 | id=self.ids.input(aio_id), 47 | value=number, 48 | type='number', 49 | min=0 50 | ), 51 | html.Div( 52 | children=fact, 53 | id=self.ids.text(aio_id) 54 | ) 55 | ]) 56 | 57 | @callback( 58 | Output(ids.text(MATCH), 'children'), 59 | Input(ids.input(MATCH), 'value') 60 | ) 61 | def update_number_fact(value): 62 | fact = get_number_fact(value) 63 | return fact 64 | -------------------------------------------------------------------------------- /src/components/navbar.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file is for creating a navigation bar that will sit at the top of your application. 4 | Much of this page is pulled directly from the Dash Bootstrap Components documentation linked below: 5 | https://dash-bootstrap-components.opensource.faculty.ai/docs/components/navbar/ 6 | ''' 7 | 8 | # package imports 9 | from dash import html, callback, Output, Input, State 10 | import dash_bootstrap_components as dbc 11 | 12 | # local imports 13 | from utils.images import logo_encoded 14 | from components.login import login_info 15 | 16 | # component 17 | navbar = dbc.Navbar( 18 | dbc.Container( 19 | [ 20 | html.A( 21 | dbc.Row( 22 | [ 23 | dbc.Col(html.Img(src=logo_encoded, height='30px')), 24 | ], 25 | align='center', 26 | className='g-0', 27 | ), 28 | href='https://plotly.com', 29 | style={'textDecoration': 'none'}, 30 | ), 31 | dbc.NavbarToggler(id='navbar-toggler', n_clicks=0), 32 | dbc.Collapse( 33 | dbc.Nav( 34 | [ 35 | dbc.NavItem( 36 | dbc.NavLink( 37 | 'Home', 38 | href='/' 39 | ) 40 | ), 41 | dbc.NavItem( 42 | dbc.NavLink( 43 | 'Complex Page', 44 | href='/complex' 45 | ) 46 | ), 47 | html.Div( 48 | login_info 49 | ) 50 | ] 51 | ), 52 | id='navbar-collapse', 53 | navbar=True 54 | ), 55 | ] 56 | ), 57 | color='dark', 58 | dark=True, 59 | ) 60 | 61 | # add callback for toggling the collapse on small screens 62 | @callback( 63 | Output('navbar-collapse', 'is_open'), 64 | Input('navbar-toggler', 'n_clicks'), 65 | State('navbar-collapse', 'is_open'), 66 | ) 67 | def toggle_navbar_collapse(n, is_open): 68 | if n: 69 | return not is_open 70 | return is_open 71 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | # notes 2 | ''' 3 | This file is for housing the main dash application. 4 | This is where we define the various css items to fetch as well as the layout of our application. 5 | ''' 6 | 7 | # package imports 8 | import dash 9 | from dash import html 10 | import dash_bootstrap_components as dbc 11 | from flask import Flask 12 | from flask_login import LoginManager 13 | import os 14 | 15 | # local imports 16 | from utils.settings import APP_HOST, APP_PORT, APP_DEBUG, DEV_TOOLS_PROPS_CHECK 17 | from components.login import User, login_location 18 | from components import navbar, footer 19 | 20 | server = Flask(__name__) 21 | app = dash.Dash( 22 | __name__, 23 | server=server, 24 | use_pages=True, # turn on Dash pages 25 | external_stylesheets=[ 26 | dbc.themes.BOOTSTRAP, 27 | dbc.icons.FONT_AWESOME 28 | ], # fetch the proper css items we want 29 | meta_tags=[ 30 | { # check if device is a mobile device. This is a must if you do any mobile styling 31 | 'name': 'viewport', 32 | 'content': 'width=device-width, initial-scale=1' 33 | } 34 | ], 35 | suppress_callback_exceptions=True, 36 | title='Dash app structure' 37 | ) 38 | 39 | server.config.update(SECRET_KEY=os.getenv('SECRET_KEY')) 40 | 41 | # Login manager object will be used to login / logout users 42 | login_manager = LoginManager() 43 | login_manager.init_app(server) 44 | login_manager.login_view = '/login' 45 | 46 | @login_manager.user_loader 47 | def load_user(username): 48 | """This function loads the user by user id. Typically this looks up the user from a user database. 49 | We won't be registering or looking up users in this example, since we'll just login using LDAP server. 50 | So we'll simply return a User object with the passed in username. 51 | """ 52 | return User(username) 53 | 54 | def serve_layout(): 55 | '''Define the layout of the application''' 56 | return html.Div( 57 | [ 58 | login_location, 59 | navbar, 60 | dbc.Container( 61 | dash.page_container, 62 | class_name='my-2' 63 | ), 64 | footer 65 | ] 66 | ) 67 | 68 | 69 | app.layout = serve_layout # set the layout to the serve_layout function 70 | server = app.server # the server is needed to deploy the application 71 | 72 | if __name__ == "__main__": 73 | app.run_server( 74 | host=APP_HOST, 75 | port=APP_PORT, 76 | debug=APP_DEBUG, 77 | dev_tools_props_check=DEV_TOOLS_PROPS_CHECK 78 | ) 79 | 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /src/components/login.py: -------------------------------------------------------------------------------- 1 | # package imports 2 | from dash import html, dcc, callback, Output, Input, State 3 | from dash.exceptions import PreventUpdate 4 | import dash_bootstrap_components as dbc 5 | from flask_login import UserMixin, current_user, logout_user, login_user 6 | 7 | class User(UserMixin): 8 | # User data model. It has to have at least self.id as a minimum 9 | def __init__(self, username): 10 | self.id = username 11 | self.role = 'student' 12 | 13 | login_card = dbc.Card( 14 | [ 15 | dbc.CardHeader('Login'), 16 | dbc.CardBody( 17 | [ 18 | dbc.Input( 19 | placeholder='Username', 20 | type='text', 21 | id='login-username', 22 | class_name='mb-2' 23 | ), 24 | dbc.Input( 25 | placeholder='Password', 26 | type='password', 27 | id='login-password', 28 | class_name='mb-2' 29 | ), 30 | dbc.Button( 31 | 'Login', 32 | n_clicks=0, 33 | type='submit', 34 | id='login-button', 35 | class_name='float-end' 36 | ), 37 | html.Div(children='', id='output-state') 38 | ] 39 | ) 40 | ] 41 | ) 42 | 43 | login_location = dcc.Location(id='url-login') 44 | login_info = html.Div(id='user-status-header') 45 | logged_in_info = html.Div( 46 | [ 47 | dbc.Button( 48 | html.I(className='fas fa-circle-user fa-xl'), 49 | id='user-popover', 50 | outline=True, 51 | color='light', 52 | class_name='border-0' 53 | ), 54 | dbc.Popover( 55 | [ 56 | dbc.PopoverHeader('Settings'), 57 | dbc.PopoverBody( 58 | [ 59 | dcc.Link( 60 | [ 61 | html.I(className='fas fa-arrow-right-from-bracket me-1'), 62 | 'Logout' 63 | ], 64 | href='/logout' 65 | ) 66 | ] 67 | ) 68 | ], 69 | target='user-popover', 70 | trigger='focus', 71 | placement='bottom' 72 | ) 73 | ] 74 | ) 75 | logged_out_info = dbc.NavItem( 76 | dbc.NavLink( 77 | 'Login', 78 | href='/login' 79 | ) 80 | ) 81 | 82 | @callback( 83 | Output('user-status-header', 'children'), 84 | Input('url-login', 'pathname') 85 | ) 86 | def update_authentication_status(path): 87 | logged_in = current_user.is_authenticated 88 | if path == '/logout' and logged_in: 89 | logout_user() 90 | child = logged_out_info 91 | elif logged_in: 92 | child = logged_in_info 93 | else: 94 | child = logged_out_info 95 | return child 96 | 97 | @callback( 98 | Output('output-state', 'children'), 99 | Output('url-login', 'pathname'), 100 | Input('login-button', 'n_clicks'), 101 | State('login-username', 'value'), 102 | State('login-password', 'value'), 103 | State('_pages_location', 'pathname'), 104 | prevent_initial_call=True 105 | ) 106 | def login_button_click(n_clicks, username, password, pathname): 107 | if n_clicks > 0: 108 | if username == 'test' and password == 'test': 109 | login_user(User(username)) 110 | return 'Login Successful', '/' 111 | return 'Incorrect username or password', pathname 112 | raise PreventUpdate 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dash-app-structure 2 | 3 | This repository is to demostrate some good practices for structuring a Dash app. 4 | 5 | ## Install and Setup 6 | 7 | For handling large apps, you'll want to make sure you are using a virtaul environment, so you can manage your packages properly. 8 | To create an environment, install requirements, and run the app, use the following: 9 | 10 | ```bash 11 | python -m venv .venv # create virtual environment 12 | .venv\Scripts\pip install -r requirements.txt # install requirements 13 | .venv\Scripts\python src\app.py # run the application 14 | ``` 15 | 16 | ## Structure 17 | 18 | This repository serves as a guide for structuring large Dash applications. 19 | The ideas presented in this guide come from my own experience working in large React projects and common attributes found in open source repositories. 20 | 21 | The following is an overview of the structure. 22 | I will follow the structure from top down, covering each item and its purpose. 23 | Additionally, I've created this repository to demonstrate the structure and serve as a template for anyone who wants to fork it. 24 | Each file in the repository includes more information about the purpose of the file. 25 | 26 | ```bash 27 | dash-app-structure 28 | |-- .venv 29 | | |-- * 30 | |-- requirements.txt 31 | |-- .env 32 | |-- .gitignore 33 | |-- License 34 | |-- README.md 35 | |-- src 36 | | |-- assets 37 | | | |-- logos/ 38 | | | |-- css/ 39 | | | |-- images/ 40 | | | |-- scripts/ 41 | | | |-- favicon.ico 42 | | |-- components 43 | | | |-- __init__.py 44 | | | |-- footer.py 45 | | | |-- navbar.py 46 | | | |-- component1.py 47 | | |-- pages 48 | | | |-- __init__.py 49 | | | |-- complex_page 50 | | | | |-- __init__.py 51 | | | | |-- layout.py 52 | | | | |-- page_specific_component.py 53 | | | |-- home.py 54 | | | |-- not_found_404.py 55 | | |-- utils 56 | | | |-- __init__.py 57 | | | |-- common_functions.py 58 | | |-- app.py 59 | ``` 60 | 61 | ### Virtual Environment 62 | 63 | The first few items in our structure refer to the virtaul environment and package manager. 64 | This is a must for handling large applications and ensuring that packages are using the correct versions. 65 | 66 | The `.venv` directory is the virtual environment itself where the project specific Python package versions are located. 67 | There are various ways to create this, but use the first command below. 68 | Note that `.venv` is a common name to use for your virtual environment. 69 | The `requirements.txt` file contains the required Python packages and their respective versions for running the application. 70 | I've included some additional commands for installing the current requirements, adding new packages, and updating the requirments file. 71 | 72 | ```bash 73 | python -m venv .venv # create the virtual environment 74 | .venv\Scripts\pip install -r requirements.txt # install all packages 75 | .venv\Scripts\pip install new_package # install a new package 76 | .venv\Scripts\pip freeze > requirements.txt # update requirements with new packages 77 | ``` 78 | 79 | Note: there is a small shift in the Python community away from using `venv` and instead using `pipenv`. 80 | At the time of writing this, I am not as familiar with `pipenv` as and I am with using `venv`. 81 | 82 | ### Environment Variables 83 | 84 | The `.env` file is where you should house any passwords or keys. 85 | This is a common practice as we do not want to directly hardcode keys into your application where a malicious actor could see them. 86 | Some common values found in `.env` files are `DATABASE_URI` or `API_KEY`. 87 | Later on in this guide, we will see how the data is loaded. 88 | 89 | ### .gitignore 90 | 91 | The `.gitignore` file is specific to using Git. 92 | If you aren't using Git, you can ignore this; however, you should be using Git or some other Version Control System. 93 | The `.gitignore` specifies files that should not be included when you commit or push your code. 94 | 95 | I use the basic Python `.gitignore` file, located at [https://github.com/github/gitignore/blob/main/Python.gitignore](https://github.com/github/gitignore/blob/main/Python.gitignore). 96 | Notice that both the `.venv` directory and `.env` file are included here. 97 | 98 | ### License 99 | 100 | The `LICENSE` file determines how your code should be shared. 101 | If you are keeping your code completely private, you do not need a license; however, you should still include one. 102 | 103 | For more inforamtion on choosing the correct license, see [https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository). 104 | 105 | ### README 106 | 107 | The `README.md` file should specify how to install and run your application. 108 | This includes, but is not limited to, installing packages, mentioning which environemnt variables need to be set, and running the application. 109 | 110 | ### src 111 | 112 | The `src` directory is where to house all of the code. 113 | We will dive into each item below. 114 | 115 | #### Assets 116 | 117 | The assets directory contains css files, javascript files, locally hosted images, site logos, and the application icon. 118 | These should be organized into folders based on their purpose. 119 | 120 | Note: some of the mentioned directories are not incldued in the sample repository as Git ignores empty directories by default. 121 | 122 | #### Components 123 | 124 | The components directory should contain common components used throughout the application. 125 | The simplest example are that of the navigation bar, `navbar.py`, and footer element, `footer.py`. 126 | All-in-one components should also be stored here. 127 | Defining each of these components in their own file is good practice. 128 | Additionally, I import each component into the `__init__.py` file. 129 | This allows for easier imports like the following: 130 | 131 | ```python 132 | from components import navbar, footer 133 | ``` 134 | 135 | #### Pages 136 | 137 | Large structured applications rely on the new Dash Pages feature, currently only available via Dash Labs. 138 | This guide will be updated once Dash Pages is included in an official Dash release. 139 | For more information, see the community post at [https://community.plotly.com/t/introducing-dash-pages-a-dash-2-x-feature-preview/57775](https://community.plotly.com/t/introducing-dash-pages-a-dash-2-x-feature-preview/57775). 140 | 141 | The pages directory houses the individual pages of your application. 142 | I split the pages into 2 categories, static and complex pages. 143 | The static pages are ones that do not have any, or minimal callbacks, such as your home, privacy policy, about, 404 not found, or contact page. 144 | The complex pages are ones that contain more complex layouts and callbacks. 145 | 146 | The static pages should be included immediately under the pages directory. 147 | While the complex pages should be included in their own directory inside the pages. 148 | 149 | See the files within the `complex_page` directory and the pages forum post for more information about how to structure more complex pages. 150 | 151 | #### Utilities 152 | 153 | The utilities directory, `utils`, is meant for common funtions run throughout the application. 154 | Splitting these up into specific files allows for more organized code. 155 | 156 | In the example repository, there are 2 files, `api.py` and `images.py`. 157 | 158 | The `api.py` file reads in our environment variables to get the `API_KEY`. 159 | The sample API called does not require a key; however, I deemed it important to include anyways. 160 | This file also defines a function that formats the inputs to call the API. 161 | To call the API, we just need to import and call the `get_number_fact(int)` method. 162 | 163 | The `images.py` file focuses on anything to do with images shown in our application. 164 | Some of the main functionality includes reading in local images and converting them to encoded strings so they show up properly. 165 | Addtionally, if you are displaying images hosted on some Content Distribution Network (CDN), you might also define a method for formatting the url here. 166 | 167 | #### App 168 | 169 | The `app.py` file is the entrypoint to defining and running the application. 170 | This is where we define the Dash app, set external stylesheets, and run the app. 171 | 172 | As it stands, Dash requires the `app` object for defining `long_callbacks`. 173 | Since this is the only place in the codebase that can access the app object, without ciruclar imports, this file should house any `long_callbacks`. 174 | --------------------------------------------------------------------------------