├── .gitignore ├── .streamlit └── config.toml ├── README.md ├── app.py ├── database.py ├── demo.jpg ├── requirements.txt ├── supermarkt_sales.xlsx └── upload_to_database.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | # End of https://www.toptal.com/developers/gitignore/api/python -------------------------------------------------------------------------------- /.streamlit/config.toml: -------------------------------------------------------------------------------- 1 | [theme] 2 | # Primary accent color for interactive elements. 3 | primaryColor = "#E694FF" 4 | 5 | # Background color for the main content area. 6 | backgroundColor = "#00172B" 7 | 8 | # Background color used for the sidebar and most interactive widgets. 9 | secondaryBackgroundColor = "#0083B8" 10 | 11 | # Color used for almost all text. 12 | textColor = "#FFF" 13 | 14 | # Font family for all text in the app, except code blocks. One of "sans serif", "serif", or "monospace". 15 | # Default: "sans serif" 16 | font = "sans serif" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Add a User Authentication Service (Login Form) in Streamlit + Database 3 | This is the second part of adding a user authentication service (login form) in Streamlit. 4 | In this part, we are reading/writing the user credentials from/to a database instead of a pickle file. 5 | In particular, we will be using the free database from deta (https://www.deta.sh/) 6 | 7 | ## Demo Website 8 | ⭐ https://www.dashboarduserauth.pythonandvba.com/ 9 | 10 | ## Video Tutorial 11 | [![YouTube Video](https://img.youtube.com/vi/eCbH2nPL9sU/0.jpg)](https://youtu.be/eCbH2nPL9sU) 12 | 13 | ## Environment Variables 14 | To run this project, you will need to add the following environment variable to your .env file 15 | `DETA_KEY` 16 | 17 | ## Watch Part 1 here 18 | [![YouTube Video](https://img.youtube.com/vi/JoFGrSRj4X4/0.jpg)](https://youtu.be/JoFGrSRj4X4) 19 | 20 | ## Screenshot 21 | ![Login Screenshot](/demo.jpg?raw=true "Login Form") 22 | 23 | ## Streamlit-authenticator 24 | ⭐ Check out the library here: https://github.com/mkhorasani/Streamlit-Authenticator 25 | 26 | 27 | 28 | ## 🤓 Check Out My Excel Add-ins 29 | I've developed some handy Excel add-ins that you might find useful: 30 | 31 | - 📊 **[Dashboard Add-in](https://pythonandvba.com/grafly)**: Easily create interactive and visually appealing dashboards. 32 | - 🎨 **[Cartoon Charts Add-In](https://pythonandvba.com/cuteplots)**: Create engaging and fun cartoon-style charts. 33 | - 🤪 **[Emoji Add-in](https://pythonandvba.com/emojify)**: Add a touch of fun to your spreadsheets with emojis. 34 | - 🛠️ **[MyToolBelt Add-in](https://pythonandvba.com/mytoolbelt)**: A versatile toolbelt for Excel, featuring: 35 | - Creation of Pandas DataFrames and Jupyter Notebooks from Excel ranges 36 | - ChatGPT integration for advanced data analysis 37 | - And much more! 38 | 39 | 40 | 41 | ## 🤝 Connect with Me 42 | - 📺 **YouTube:** [CodingIsFun](https://youtube.com/c/CodingIsFun) 43 | - 🌐 **Website:** [PythonAndVBA](https://pythonandvba.com) 44 | - 💬 **Discord:** [Join our Community](https://pythonandvba.com/discord) 45 | - 💼 **LinkedIn:** [Connect with me](https://www.linkedin.com/in/sven-bosau/) 46 | - 📸 **Instagram:** [Follow me](https://www.instagram.com/codingisfun_official/) 47 | 48 | ## Support My Work 49 | Love my content and want to show appreciation? Why not [buy me a coffee](https://pythonandvba.com/coffee-donation) to fuel my creative engine? Your support means the world to me! 😊 50 | 51 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://pythonandvba.com/coffee-donation) 52 | 53 | ## Feedback 54 | Got some thoughts or suggestions? Don't hesitate to reach out to me at contact@pythonandvba.com. I'd love to hear from you! 💡 55 | ![Logo](https://www.pythonandvba.com/banner-img) 56 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import pandas as pd # pip install pandas openpyxl 2 | import plotly.express as px # pip install plotly-express 3 | import streamlit as st # pip install streamlit 4 | import streamlit_authenticator as stauth # pip install streamlit-authenticator 5 | 6 | import database as db 7 | 8 | 9 | # emojis: https://www.webfx.com/tools/emoji-cheat-sheet/ 10 | st.set_page_config(page_title="Sales Dashboard", page_icon=":bar_chart:", layout="wide") 11 | 12 | # --- DEMO PURPOSE ONLY --- # 13 | placeholder = st.empty() 14 | placeholder.info("CREDENTIALS | username:pparker ; password:abc123") 15 | # ------------------------- # 16 | 17 | # --- USER AUTHENTICATION --- 18 | users = db.fetch_all_users() 19 | 20 | usernames = [user["key"] for user in users] 21 | names = [user["name"] for user in users] 22 | hashed_passwords = [user["password"] for user in users] 23 | 24 | authenticator = stauth.Authenticate(names, usernames, hashed_passwords, 25 | "sales_dashboard", "abcdef", cookie_expiry_days=30) 26 | 27 | name, authentication_status, username = authenticator.login("Login", "main") 28 | 29 | if authentication_status == False: 30 | st.error("Username/password is incorrect") 31 | 32 | if authentication_status == None: 33 | st.warning("Please enter your username and password") 34 | 35 | if authentication_status: 36 | placeholder.empty() 37 | # ---- READ EXCEL ---- 38 | @st.cache 39 | def get_data_from_excel(): 40 | df = pd.read_excel( 41 | io="supermarkt_sales.xlsx", 42 | engine="openpyxl", 43 | sheet_name="Sales", 44 | skiprows=3, 45 | usecols="B:R", 46 | nrows=1000, 47 | ) 48 | # Add 'hour' column to dataframe 49 | df["hour"] = pd.to_datetime(df["Time"], format="%H:%M:%S").dt.hour 50 | return df 51 | 52 | df = get_data_from_excel() 53 | 54 | # ---- SIDEBAR ---- 55 | authenticator.logout("Logout", "sidebar") 56 | st.sidebar.title(f"Welcome {name}") 57 | st.sidebar.header("Please Filter Here:") 58 | city = st.sidebar.multiselect( 59 | "Select the City:", 60 | options=df["City"].unique(), 61 | default=df["City"].unique() 62 | ) 63 | 64 | customer_type = st.sidebar.multiselect( 65 | "Select the Customer Type:", 66 | options=df["Customer_type"].unique(), 67 | default=df["Customer_type"].unique(), 68 | ) 69 | 70 | gender = st.sidebar.multiselect( 71 | "Select the Gender:", 72 | options=df["Gender"].unique(), 73 | default=df["Gender"].unique() 74 | ) 75 | 76 | df_selection = df.query( 77 | "City == @city & Customer_type ==@customer_type & Gender == @gender" 78 | ) 79 | 80 | # ---- MAINPAGE ---- 81 | st.title(":bar_chart: Sales Dashboard") 82 | st.markdown("##") 83 | 84 | # TOP KPI's 85 | total_sales = int(df_selection["Total"].sum()) 86 | average_rating = round(df_selection["Rating"].mean(), 1) 87 | star_rating = ":star:" * int(round(average_rating, 0)) 88 | average_sale_by_transaction = round(df_selection["Total"].mean(), 2) 89 | 90 | left_column, middle_column, right_column = st.columns(3) 91 | with left_column: 92 | st.subheader("Total Sales:") 93 | st.subheader(f"US $ {total_sales:,}") 94 | with middle_column: 95 | st.subheader("Average Rating:") 96 | st.subheader(f"{average_rating} {star_rating}") 97 | with right_column: 98 | st.subheader("Average Sales Per Transaction:") 99 | st.subheader(f"US $ {average_sale_by_transaction}") 100 | 101 | st.markdown("""---""") 102 | 103 | # SALES BY PRODUCT LINE [BAR CHART] 104 | sales_by_product_line = ( 105 | df_selection.groupby(by=["Product line"]).sum()[["Total"]].sort_values(by="Total") 106 | ) 107 | fig_product_sales = px.bar( 108 | sales_by_product_line, 109 | x="Total", 110 | y=sales_by_product_line.index, 111 | orientation="h", 112 | title="Sales by Product Line", 113 | color_discrete_sequence=["#0083B8"] * len(sales_by_product_line), 114 | template="plotly_white", 115 | ) 116 | fig_product_sales.update_layout( 117 | plot_bgcolor="rgba(0,0,0,0)", 118 | xaxis=(dict(showgrid=False)) 119 | ) 120 | 121 | # SALES BY HOUR [BAR CHART] 122 | sales_by_hour = df_selection.groupby(by=["hour"]).sum()[["Total"]] 123 | fig_hourly_sales = px.bar( 124 | sales_by_hour, 125 | x=sales_by_hour.index, 126 | y="Total", 127 | title="Sales by hour", 128 | color_discrete_sequence=["#0083B8"] * len(sales_by_hour), 129 | template="plotly_white", 130 | ) 131 | fig_hourly_sales.update_layout( 132 | xaxis=dict(tickmode="linear"), 133 | plot_bgcolor="rgba(0,0,0,0)", 134 | yaxis=(dict(showgrid=False)), 135 | ) 136 | 137 | 138 | left_column, right_column = st.columns(2) 139 | left_column.plotly_chart(fig_hourly_sales, use_container_width=True) 140 | right_column.plotly_chart(fig_product_sales, use_container_width=True) 141 | 142 | 143 | # ---- HIDE STREAMLIT STYLE ---- 144 | hide_st_style = """ 145 | 150 | """ 151 | st.markdown(hide_st_style, unsafe_allow_html=True) 152 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from deta import Deta # pip install deta 4 | from dotenv import load_dotenv # pip install python-dotenv 5 | 6 | 7 | # Load the environment variables 8 | load_dotenv(".env") 9 | DETA_KEY = os.getenv("DETA_KEY") 10 | 11 | # Initialize with a project key 12 | deta = Deta(DETA_KEY) 13 | 14 | # This is how to create/connect a database 15 | db = deta.Base("users_db") 16 | 17 | 18 | def insert_user(username, name, password): 19 | """Returns the user on a successful user creation, otherwise raises and error""" 20 | return db.put({"key": username, "name": name, "password": password}) 21 | 22 | 23 | def fetch_all_users(): 24 | """Returns a dict of all users""" 25 | res = db.fetch() 26 | return res.items 27 | 28 | 29 | def get_user(username): 30 | """If not found, the function will return None""" 31 | return db.get(username) 32 | 33 | 34 | def update_user(username, updates): 35 | """If the item is updated, returns None. Otherwise, an exception is raised""" 36 | return db.update(updates, username) 37 | 38 | 39 | def delete_user(username): 40 | """Always returns None, even if the key does not exist""" 41 | return db.delete(username) 42 | -------------------------------------------------------------------------------- /demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sven-Bo/streamlit-sales-dashboard-with-userauthentication-database/e6cb9579b3785a6ccfa3c8dc7283e568b79daae2/demo.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit_authenticator==0.1.5 2 | streamlit==1.12.0 3 | openpyxl==3.0.10 4 | plotly==4.14.3 5 | deta==1.1.0 6 | pandas==1.4.3 7 | python-dotenv==0.21.0 8 | -------------------------------------------------------------------------------- /supermarkt_sales.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sven-Bo/streamlit-sales-dashboard-with-userauthentication-database/e6cb9579b3785a6ccfa3c8dc7283e568b79daae2/supermarkt_sales.xlsx -------------------------------------------------------------------------------- /upload_to_database.py: -------------------------------------------------------------------------------- 1 | import streamlit_authenticator as stauth 2 | 3 | import database as db 4 | 5 | usernames = ["pparker", "rmiller"] 6 | names = ["Peter Parker", "Rebecca Miller"] 7 | passwords = ["abc123", "def456"] 8 | hashed_passwords = stauth.Hasher(passwords).generate() 9 | 10 | 11 | for (username, name, hash_password) in zip(usernames, names, hashed_passwords): 12 | db.insert_user(username, name, hash_password) --------------------------------------------------------------------------------