Try this simple Routing and Auth approach for Streamlit
Streamlit is a useful tool to create and share visual reports using Python. The library does all the heavy lifting from creating a web server to building HTML components using simple Python commands. It is not a lie to say that you can have a Streamlit up and running locally in your browser using just two lines of code.
However you will come up to the point when you need to pass some data to the webapp to filter or render something different given a changing input. Or perhaps you don’t want your webapp to be fully accesible to the whole world upon deploying it.
Streamlit does not have a straightforward way to achieve these functionalities, so this post will show an approach to tackle these tasks in a convenient way.
First lets state the problem. Assume you have a table holding daily data from impressions and clicks for two different customers: Shoes Inc and Shirts.com. You need a Streamlit app with a table containing such a report to each customer.
The pseudo code should do something like this.
- Get the data.
- Receive the input and validate it.
- Filter the data to show the required information.
It does not sound so hard, so lets get it done. For this tutorial I will assume you have a Python project with Streamlit and Pandas installed. If you want some a more basic tutorial take a look at this one.
Get the data (and build the Streamlit app)
For the sake of simplicity I will just generate some random data with a function and assign it to a variable. This is the relevant code, stored in a routing_example.py file.
import streamlit as st
import pandas as pd
import random
import numpy as np
def build_dataframe(rows_count=100):
"""
Creates columns with random data.
"""
data = {
'impressions': np.random.randint(low=111, high=10000, size=rows_count),
'clicks': np.random.randint(low=0, high=1000, size=rows_count),
'customer': random.choices(['ShirtsInc', 'ShoesCom'], k=rows_count)
}
df = pd.DataFrame(data)
# add a date column and calculate the weekday of each row
df['date'] = pd.date_range(start='1/1/2018', periods=rows_count)
return df
data_df = build_dataframe()
st.title('Streamlit routing dashboard')
st.dataframe(data_df)
This code doest not only generates the data but also imports the required libraries and builds a super basic Streamlit app that displays the data in a tabular form using Streamlit’s st.dataframe() method.
After executing streamlit run routing_example.py the app is available in the browser.
It looks good! However neither Shirts Inc nor Shoes.com will be very happy when visiting it for two quite important reasons.
- When this app is deployed everyone in the world with the web app URL can access the reported information.
- Shirts Inc can see Shoes.com information and the other way around.
It is better to keep customers happy so what is the solution? Well, as stated before: we are required to somehow query wether it is Shirts Inc or Shoes.com the one trying to view the report and then to filter the table so that only one company’s data is shown. These are steps 2 and 3.
Receive the input and validate it
In web development there is something called query strings or URL parameters. These are strings appended to an URL that can be read by the server or front end web application, such as React.js or Angular, to achieve some functionality. You can find tons of references about it.
Query strings look like this.
http://localhost:8501/?token=abc123&view=report
You have the root URL before the ‘?’ sign and then a set of name=value pairs separated by an ‘&’. Quite simple. Now comes the good part, Streamlit can read query strings with a st.experimental_get_query_params() method.
Add these lines at the bottom of your routing_example.py and visit the URL above.
query_params = st.experimental_get_query_params()
st.write(query_params)
And… This is the result!
The st.write() method renders a dictionary as the output of the query_params var. This is just great because it allows us to somehow receive information from the user before rendering anything. Get the idea? With these variables you can trigger a ton of functionalities, even for this simple Streamlit app.
Note that each dictionary key maps to a list. Does this looks suspicious to you? Does that mean that it might be possible to add more than one value to each variable name? We should do an experiment by rendering the Streamlit app with this query string. Note that we are declaring two values for the product variable: shoes and shirts.
http://localhost:8501/?token=abc123&product=shoes&product=shirts
This is the output. Now there is a product key mapping to a list containing the two product query string values. You have to agree this is powerful.
Enough experimentation. Let’s use this feature to make Shirts Inc nor Shoes.com happy.
Filter the data to show the required information
So far we know that data can be sent to Streamlit before rendering anything using query strings. Let’s recap by remembering that we want to show to Shirts Inc and Shoes.com their own data, but only after verifying the visitor is either of them. How can we accomplish this?
Again, lets get some inspiration from web development. When developing a web app, usernames and passwords are used to generate access tokens as follows.
- You visit a page and are received with a username and password form.
- You type in your credentials and then a request is sent to the backend.
- The backend checks that the username exists and that it is associated to the provided password. If both conditions are met an access token is provided and the user is redirected to the protected sections of the web application.
Can we replicate this process with Streamlit and query strings? You bet it is possible. Look the following query string and verify it has the data required to accomplish the previous process.
http://localhost:8501/?username=ShirtsInc&password=shirtspassword&view=report
We have a username which existence can be verified and a password to check for a match. There is also a variable view to tell Streamlit where is the user willing to go. How can the username and password be checked? Well, there are several ways to do it, including making a query to a database; however to keep this simple we are going to store a credentials dictionary inside the code where the username and corresponding passwords are stored. With the URL above, what we know about Streamlit and the credentials dictionary the rest of the job can be accomplished with a few Python lines.
The code below is the one that achieves the required functionality. Take a look at it, I believe the inline comments tell the story right.
import streamlit as st
import pandas as pd
import random
import numpy as np
def build_dataframe(rows_count=100):
"""
Creates columns with random data.
"""
data = {
'impressions': np.random.randint(low=111, high=10000, size=rows_count),
'clicks': np.random.randint(low=0, high=1000, size=rows_count),
'customer': random.choices(['ShirtsInc', 'ShoesCom'], k=rows_count)
}
df = pd.DataFrame(data)
# add a date column and calculate the weekday of each row
df['date'] = pd.date_range(start='1/1/2018', periods=rows_count)
return df
data_df = build_dataframe()
query_params = st.experimental_get_query_params()# There is only one value for each parameter, retrieve the one at # # index 0
username = query_params.get('username', None)[0]
password = query_params.get('password', None)[0]
view = query_params.get('view', None)[0]
# Super basic (and not recommended) way to store the credentials
# Just for illustrative purposes!
credentials = {
'ShoesCom': 'shoespassword',
'ShirtsInc': 'shirtspassword'
}
logged_in = False
# Check that the username exists in the "database" and that the provided password matches
if username in credentials and credentials[username] == password:
logged_in = True
if not logged_in:
# If credentials are invalid show a message and stop rendering the webapp
st.warning('Invalid credentials')
st.stop()
available_views = ['report']
if view not in available_views:
# I don't know which view do you want. Beat it.
st.warning('404 Error')
st.stop()# The username exists and the password matches!
# Also, the required view exists
# Show the webapp
st.title('Streamlit routing dashboard')
# IMPORTANT: show only the data of the logged in customer
st.dataframe(data_df[data_df['customer'] == username])
There are three things worth mentioning. The first one is the use of the st.stop() method after some condition fails. This method prevents the browser from rendering the code below this instruction. The second is the use of the username to filter the data and show only the corresponding user’s data. The last one is the routing functionality achieved by adding a view query string and a validation code. In this simple webapp we have only one view, however a more complex Streamlit app may have more. In any case the code can decide, given the view value, what to show to the visitor.
Now here comes the fun part. Let’s try the code.
Visit this two URLs and the web app will work. Remember to do so having your Streamlit app running.
http://localhost:8501/?username=ShirtsInc&password=shirtspassword&view=report
http://localhost:8501/?username=ShoesCom&password=shoespassword&view=report
Here is the evidence of the second one working properly. Note that, given that ShoesCom username exists and the password match, Streamlit shows only the data belonging to Shoes.com! Looks good.
And this is the extra fun part: breaking the code. Try this other two URLs.
http://localhost:8501/?username=InvalidUsername&password=shoespassword&view=report
http://localhost:8501/?username=ShoesCom&password=shoespassword&view=invalid_view
There you go! An error is shown for each one of them but for different reasons.
I believe it can be said that the web app works as expected: the users are (primitively) authenticated and only the authenticated user’s data is shown.
Note: this code will really break if you don’t provide the username, password or view variables in the query string. But hey! It can be fixed and the rest works.
Conclusions
Streamlit is not a web development framework but a data visualization tool, with methods powerful enough to allow a, maybe not very sophisticated, routing and authentication functionalities. The credentials and views are all stored in the code for illustrative purposes, however more code can be written to retrieve it from somewhere else such a database. With query strings navigating functionality could be added by inserting buttons with anchors. The idea is simple but the functionalities that can be achieved are quite extensive!
Thanks for reading. If you find this useful please share!