How I Created a Weekly Shopping List and Recipe App in Streamlit

5/5 - (5 votes)
How I Created a Weekly Shopping List and Recipe App in Streamlit

Project Description

So I’ve been toying with the idea of creating a shopping list application for a while now.

I wanted to create it in such a way that everybody in the family could contribute. We order all our food once a week, and then I go and collect it.

With the app, everybody would have a say in that week’s choice of dinners. This led to the conclusion that we would need a recipe section also for people to choose from or add to.

Here’s how the final app looks:

👉 Try It Yourself: Feel free to check out the live demo app here.

Also, you can check out the Finxter programming course on the same topic with a downloadable PDF certificate with your name on it to prove your skills to the world:

Install Dependencies

The first thing for us to do is install the PIP packages that are not built into Python itself. If you run the commands below in your terminal this should take care of it.

I’ll go into more detail on what each of these does as we encounter them.

pip install streamlit
pip install streamlit_option_menu
pip install isoweek
Pip install Deta

Step 1: Set Up the Directory Structure

I use VS Code as my editor of choice, and setting up a new project folder there is a breeze. Right-click in the Explorer menu on the left side and click New Folder. After that, we set up the folder structure as shown below.

├── .streamlit
│   ├── config.toml
│   └── secrets.toml
├── db.py
|── shopping_list.py

Make sure you don’t forget to add the ‘.’ in front of the .streamlit folder name. This folder will keep our configuration and secret files for the app to function.

The shopping_list.py is our main application file, while the db.py file will hold all the logic used for interacting with the Deta database.

Step 2: Import dependencies and Streamlit basic set-up

Our initial work will be done in the shopping_list.py file. At the top, we import everything we need.

#---PIP PACKAGES---#
import streamlit as st
from streamlit_option_menu import option_menu
from isoweek import Week


#---BUILT-IN PYTHON MODULES
from datetime import datetime, date
import calendar
import uuid


#---IMPORT THE DATABASE PYTHON FILE db.py---#
import db as db

The next step in the process is for us to create a basic Streamlit app that we can launch. Every time we change something in our code, we’ll see the effect this has after refreshing the browser window. 

#---STREAMLIT SETTINGS---#
page_title = "Weekly dinner and shopping app"
page_icon = ":pouch:"
layout = "centered"

In the #---STREAMLIT SETTINGS---# block, we define the variables to initialize the app. This makes it nice and tidy, plus it is easy to change if you decide later to tinker with them.

#---STREAMLIT PAGE CONFIG---#
st.set_page_config(page_title=page_title, page_icon=page_icon, layout=layout)
st.title(f"{page_title} {page_icon}")

We use the variables in the next block,  #---STREAMLIT PAGE CONFIG---#, to initialize the app. The st.set_page_config() function is always run at the beginning of a Streamlit app. If you don’t do this, Streamlit will throw an error.

#---STREAMLIT CONFIG HIDE---#
hide_st_style = """<style>
                #MainMenu {visibility : hidden;}
                footer {visibility : hidden;}
                header {visibility : hidden;}
                </style>
                """
st.markdown(hide_st_style, unsafe_allow_html=True)

The last block, #---STREAMLIT CONFIG HIDE---#, is optional but hides the “Made with Streamlit” banner at the bottom and the hamburger menu at the top if you want to.

If you now run the command below in the root of your project folder, a browser window should open for you. If everything went well you should see the page title at the top of an otherwise empty page.

streamlit run shopping_list.py

Step 3: Creating the variables

For the app to work, I used multiple dictionaries. We’ll be creating those next. To save you a lot of typing 🙂, I’ll insert all of the code below, and you can just copy and paste it.

#---DICT INIT---#
shopping_list = {
    "fruit_and_veg" : {"title" : "Fruit and Veggies", "items" : []},
    "meat_and_fish" : {"title" :"Fresh meat and fish", "items" : []},
    "housekeeping" : {"title" :"Housekeeping supplies", "items" : []},
    "carbs" : {"title" : "Potatoes, rice, pasta, etc", "items" : []},
    "snacks" : {"title" : "Snacks", "items" : []},
    "dairy" : {"title" : "Dairy", "items" : []},
    "personal_care" : {"title" : "Personal care", "items" : []},
    "pets" : {"title" : "Pets", "items" : []},
    "beverages" : {"title" : "Beverages", "items" : []},
    "spices_and_cond" : {"title" : "Spices and condiments", "items" : []},
    "frozen" : {"title" : "Frozen", "items" : []}
    }


key_dict = {
    'fruit_and_veg': [],
    'meat_and_fish': [],
    'housekeeping': [],
    'carbs': [],
    'snacks': [],
    'dairy': [],
    'personal_care': [],
    'pets': [],
    'beverages': [],
    'spices_and_cond': [],
    'frozen': []
    }
ingredients_dict = {}


instructions_dict = {}

We’ll also need another set of variables to keep track of the current week and get its week number. Luckily the datetime, calendar, and isoweek modules have us covered!

#---PERIOD VALUES---#
year = datetime.today().year
month = datetime.today().month
day = datetime.today().day+4
months = list(calendar.month_name[1:])
week_number = date(year, month, day).isocalendar()[1]
week = Week(year, week_number)
week_plus1 = Week(year, week_number+1)

Step 4: Setting up Deta Base

For our database, we’ll be using an awesome service called Deta. They offer free Cloud computing units, NoSQL databases, and a file storage service. Setting up an account only takes a minute, and afterward, you are ready to start.

The first thing we need to do is set up an API key so we can set up the connection between our application and Deta. Click the “Project Keys” on the left side and then “Create Key”. Make sure to save it somewhere, as you will need it shortly. 

Step 5: Storing the API Key securely

The next step is creating the functionality that will allow us to interact with the database itself. For that to work, we need to set up the connection to the database first. For this, we will be using the Streamlit Secrets module. This allows us to securely store the key and call it when we need it later.

Navigate to the secrets.toml file situated in the .streamlit folder. Once there you create a DETA_KEY variable and assign the API Key you saved earlier to it. Don’t forget to add quotation marks around it!

DETA_KEY = "<DETA API KEY>"

Step 6: Building out the database functions for the shopping list

With all the preparatory work done, we can start on the good stuff! The first order of business is opening the empty db.py file and importing everything we need to get this to work.

from deta import Deta
from pprint import pprint
import streamlit as st

The pprint module is not required but helps out a lot in printing out dictionaries in a readable way. This helps a lot, I can assure you 🙂.

After the import, we are ready to connect to the database. The code below will first retrieve the key from the secrets.toml file and assign it to the variable DETA_KEY. Next, it will use that variable to create a Deta object.

DETA_KEY = st.secrets.DETA_KEY
deta = Deta(DETA_KEY)

Once this is done, it takes only two lines of code to create the two Bases (Deta’s equivalent of tables or collections) I use in this project. 

sl = deta.Base("sl")
recipes = deta.Base("recipes")

 The sl variable will hold our shopping list items, while the recipes variable will be the place to store all the recipes. The simple fact of declaring the variables will create the Bases in Deta.

You can check this in the Deta Dashboard. Under the Bases on the left side, you will now see the names you declared earlier.

The first two functions will either add a week’s shopping list to the database or retrieve it.

Deta uses put to insert data. The function accepts a dictionary with an optional key argument. If you don’t pass a key, it will be generated automatically.

After that, you can pass one or more key-value pairs. In our case, I will pass two extra pairs containing a title and the shopping list dictionary.

As the key needs to be a string, I typecast it here to avoid any potential errors.

#---SHOPPING LIST FUNCTIONS---#


def enter_shopping_list_items(weeknumber, title, shopping_list):
    """This function will add all items in the shopping_list dictionary and the week title to the database with the week number as a key. Will return None if successful. """
    return sl.put({"key" : str(weeknumber),"title":title, "shopping_list" :shopping_list})

To be able to get back the shopping list that is created with the previous function, I define the get_shopping_list function. It takes one argument, which is the title from the previous function.

def get_shopping_list(period):
    """Returns the entire shopping list item for a certain week based on the period. This period is the week title used in the function above"""
    return sl.get(str(period))

We add additional items to a week’s shopping list by calling update_shopping_list. It takes a key that we defined in our first function as the weeknumber. A second parameter is a dictionary with key-value pairs of items that you want to change.

For this function, we only need to add items to that week’s shopping list.

We create a new dictionary by iterating over the key and values in tandem.

Then I call the util.append method on each of the values, which contain the actual items we want to add. As a last step, it suffices to call the Deta class method update to add all new items to our Base.

def update_shopping_list(weeknumber, update_dict):
    """Updating values in Deta Base only takes the key of the items you already created  and a dictionary of key-value pairs that can add, modify or delete the original data in the database."""
    for key, value in update_dict.items():
        print(f"{key} {value} ")
        shopping_list_update_line = {
            f"shopping_list.{key}.items": sl.util.append(value)
            }
           
        sl.update(shopping_list_update_line, weeknumber)

To complete the CRUD quartet, we need one more function that I will define as the remove_item_shopping_list function. The way I got this to work is to use the previously defined get_shopping_list function.

It retrieves a single dictionary defined by the category. That dictionary will contain the items for that category. To remove our item, I just call the dictionary remove method.  Once done, it’s a matter of calling the Deta update method again to remove the item from the database.

def remove_item_shopping_list(weeknumber, item_cat, item_to_remove):
    """Function  to remove a single item from the list. It takes a weeknumber as the principal key to get the list for the correct week.
    The item cat selects the the category and the item_to_remove is the actual item to get rid off. The same update function as above is
    then used to apply the changes."""
    shopping_list_to_change = get_shopping_list(weeknumber)["shopping_list"][item_cat]['items']
    shopping_list_to_change.remove(item_to_remove)
    print(shopping_list_to_change)
   
    shopping_list_update_line = {
        f"shopping_list.{item_cat}.items": shopping_list_to_change
        }


    sl.update(shopping_list_update_line, weeknumber)

Step 7: Building out the database functions for the recipes

After coding out every function needed for the shopping list, it is time to do the same for the recipes. Using Deta’s Python SDK it is very easy to code out two functions to get all recipes from the database or insert a new recipe into it.

The get_recipes function uses one line of code to retrieve a list of all the recipe objects that are in the recipes Base. The fact that it is a list will come in handy later for determining if there are any recipes already in that Base or not.

#---RECIPE FUNCTIONS


def get_recipes():
    """Calling the fetch method from Deta to retrieve all recipes from the database."""
    return recipes.fetch().items

As my second function, I define enter_recipe. It uses an identical layout as the enter_shopping_list function before. The recipe name will be the key. The ingredients and instructions are dictionaries with lists as values.  

def enter_recipe(name, ingredients, instructions, active=False):
    """Using the Deta put method with the recipe name as the key, this function inserts the ingredients and instructions into the database.
    The default False value for the active parameter will be used to determine if the recipe is in this week’s column or not."""
    return recipes.put({"key" : name,"ingredients" : ingredients, "instructions" :instructions, "active" : active})

To make sure that we can add or remove recipes from the current week’s shopping list, we need two functions.

The get_recipe_status function will determine in what column a recipe should currently reside. It will then add it to the list associated with that column. At the end, it will then return that list.

For us to be able to change the status of a recipe, I define the update_recipe_status function. Its only purpose is to take a recipe key parameter and then invert the current boolean variable. 

def get_recipe_status(col_nr = "a"):
    """A function to easily get the status of a recipe and append it to the correct list used in the shopping_list.py file. The col_nr parameter is used to determine the column a recipe is placed into."""
    needed_recipe_list = []
    if col_nr =="a":
        for recipe in get_recipes():
            if recipe["active"] == True:
                needed_recipe_list.append(recipe)
    elif col_nr =="b":
        for recipe in get_recipes():
            if recipe["active"] == False:
                needed_recipe_list.append(recipe)
    return needed_recipe_list


def update_recipe_status(key):
    """This function will change the boolean active variable and just inverts the current boolean status."""
    to_change= recipes.get(key)
    if to_change["active"] == False:
        changed = {"active" : True}
        recipes.update(changed, key)
    else:
        changed = {"active" : False}
        recipes.update(changed, key

The last function of our db.py file will add all the ingredients of a recipe to the current week’s shopping list. It takes a recipe key as its first parameter and a weeknumber as its second parameter. Remember that week numbers are the identifying key for shopping list objects from the sl Base.

def add_ingredients_to_shopping_list(key, weeknumber):
    """This function will retrieve all the ingredients of the selected recipe and then call the Deta update method to add those ingredients to this week's shopping list"""
    ingredients_to_add= recipes.get(key)
    for ingredient in ingredients_to_add["ingredients"]:
        shopping_list_update_line = {
            "shopping_list.snacks.items": sl.util.append(ingredient.strip("-"))
            }
   
        sl.update(shopping_list_update_line, weeknumber)

If everything goes well, the completed db.py file should look like this.

from deta import Deta
from pprint import pprint
import streamlit as st


DETA_KEY = st.secrets.DETA_KEY
deta = Deta(DETA_KEY)


sl = deta.Base("sl")
recipes = deta.Base("recipes")


#---SHOPPING LIST FUNCTIONS---#


def enter_shopping_list_items(weeknumber, title, shopping_list):
    """This function will add all items in the shopping_list dictionary and the week title to the database with the week number as a key. Will return None if successful. """
    return sl.put({"key":str(weeknumber),"title":title, "shopping_list":shopping_list})


def get_shopping_list(period):
    """Returns the entire shopping list item for a certain week based on the period. This period is the week title used in the function above."""
    return sl.get(str(period))


def update_shopping_list(weeknumber, update_dict):
    """Updating values in Deta Base only takes the key of the items you already created  and a dictionary of key-value pairs that can add, modify or delete the original data in the database."""
    for key, value in update_dict.items():
        print(f"{key} {value} ")
        shopping_list_update_line = {
            f"shopping_list.{key}.items": sl.util.append(value)
            }
           
        sl.update(shopping_list_update_line, weeknumber)


def remove_item_shopping_list(weeknumber, item_cat, item_to_remove):
    """Function  to remove a single item from the list. It takes a weeknumber as the principal key to get the list for the correct week. The item_cat selects the category and the item_to_remove is the actual item to get rid off. The same update function as above is then used to apply the changes."""
    shopping_list_to_change = get_shopping_list(weeknumber)["shopping_list"][item_cat]['items']
    shopping_list_to_change.remove(item_to_remove)
    print(shopping_list_to_change)
   
    shopping_list_update_line = {
        f"shopping_list.{item_cat}.items": shopping_list_to_change
        }


    sl.update(shopping_list_update_line, weeknumber)


#---RECIPE FUNCTIONS


def get_recipes():
    """Calling the fetch method from Deta to retrieve all recipes from the database."""
    return recipes.fetch().items


def enter_recipe(name, ingredients, instructions, active=False):
    """Using the Deta put method with the recipe name as the key, this function inserts the ingredients and instructions into the database.
    The default False value for the active parameter will be used to determine if the recipe is in this week’s column or not."""
    return recipes.put({"key" : name,"ingredients" : ingredients, "instructions" :instructions, "active" : active})


def get_recipe_status(col_nr = "a"):
    """A function to easily get the status of a recipe and append it to the correct list used in the shopping_list.py file. The col_nr parameter is used to determine the column a recipe is placed into."""
    needed_recipe_list = []
    if col_nr =="a":
        for recipe in get_recipes():
            if recipe["active"] == True:
                needed_recipe_list.append(recipe)
    elif col_nr =="b":
        for recipe in get_recipes():
            if recipe["active"] == False:
                needed_recipe_list.append(recipe)
    return needed_recipe_list


def update_recipe_status(key):
    """This function will change the boolean active variable and just inverts the current boolean status."""
    to_change= recipes.get(key)
    if to_change["active"] == False:
        changed = {"active" : True}
        recipes.update(changed, key)
    else:
        changed = {"active" : False}
        recipes.update(changed, key


def add_ingredients_to_shopping_list(key, weeknumber):
    """This function will retrieve all the ingredients of the selected recipe and then call the Deta update method to add those ingredients to this week's shopping list"""
    ingredients_to_add= recipes.get(key)
    for ingredient in ingredients_to_add["ingredients"]:
        shopping_list_update_line = {
            "shopping_list.snacks.items": sl.util.append(ingredient.strip("-"))
            }
   
        sl.update(shopping_list_update_line, weeknumber)

Tip: If you want to test any of the functions you wrote up until now you can do that from within the db.py file. Just remember to insert at least one shopping list and one recipe first 😝.

Step 8: Building out the Streamlit app

Before we do all the coding work for the weekly shopping list and recipes we need to define a quick navigation menu.

Making this work is as easy as calling the option_menu method we imported earlier and defining the variables. For the icons, you can just choose whatever suits you from Bootstraps Icon Library as they are free.

#---NAV BAR---#
nav_menu = option_menu(
    menu_title = None,
    options = ["Current Week", "Weekly recipes"],
    icons = ["list-task", "cup-straw" ],
    orientation = "horizontal"
)

Quickly running streamlit run shopping_list.py should yield something similar to the screenshot below.

Working with Streamlit is quite intuitive once you get the hang of it.

For example, the two tabs we defined in the navbar above are just two large if-statements. Clicking one of the two buttons in this menu will set the nav_menu variable equal to one of the two values from the options list.

After that, the code block within the if-statement runs.

if nav_menu == "Current Week":
    """code block for the shopping list tab"""


if nav_menu == "Weekly recipes":  
    """code block for the recipes tab"""   

Step 9: Building the shopping list tab

To begin the shopping list tab, we start by creating a header. Its content will be generated dynamically, based on the #---PERIOD VALUES---# we defined earlier.

Next are the two columns we’ll need to represent the input form on the left side and the list with items on the shopping list on the right side.

if nav_menu == "Current Week":
    """code block for the shopping list tab"""
   
    st.header(f"Thursday {week.thursday()} to Wednesday {week_plus1.wednesday()}")
    col1, col2 = st.columns([4,4], gap = "medium")

Working in a Streamlit element, like our columns, is usually achieved through a with-statement.

Within the first column, we first add a caption. This is a Streamlit method used to annotate or explain something on the screen. In our case, I use it to make users aware that they need to separate items by commas when they enter them.

As with the column, we create the form element with another with-statement. It takes two parameters which are the key and clear_on_submit. We set the second parameter to True to make it possible to enter items for the shopping list multiple times without having to reload the page.

Because we created the shopping_list dictionary earlier, we can now create the entire input form. I get all its categories by simply iterating over the items. The last and mandatory element of a Streamlit form is the form_submit_button. It needs to be at the very bottom of the form.

with col1:
        st.caption(f"Please enter items, separated by commas")
        with st.form("entry_form", clear_on_submit=True):
                       
            for k, value in shopping_list.items():
                st.text_input(f"{shopping_list[k]['title']}:", key=k)
            "---"
           
            submitted = st.form_submit_button("Save shopping list items", type = "primary")  

The result will be a nicely formatted form with a submit button on the bottom.

Making sure the submit button works as it needs will take us a couple of lines of code 🙂. In essence, I first check if it was clicked. If this is the case, all the code within that if-statement gets run.

Pressing that button also passes everything entered into the Streamlit session_state variable as key-value pairs. These pairs are then accessible later in your code, as we will now do below.

The first thing I check is if a shopping list for the current week exists already. If this is the case, we use the key_dict, we defined at the start of the shopping_list.py file.

It provides the keys we’ll need to create the update_dict. Getting the values for each of the keys is as simple as checking if session_state for that key is not empty and then parse it. If I don’t check for an empty state, previous items for a certain category could be deleted as they are overwritten by an empty list

Adding the caption earlier to separate individual items by commas now comes into play. Using those commas, I split up items for a certain category and then strip off the spaces. Afterward, the list of these parsed items gets assigned to one of the keys from the key_dict.

This dictionary will then be passed to the update_shopping_list function to add the items we entered to the current week’s Base object.  

if submitted:
                if db.get_shopping_list(week_number):
                   
                    for key in key_dict:
                        update_dict = {}
                        if st.session_state[key] != '':
                                items = st.session_state[key].split(",")
                                for item in items:
                                    item = item.strip()
                                update_dict[key] = items                  
                                db.update_shopping_list(str(week_number), update_dict)

On the other hand, it is possible that this is the first time I’m adding items to the current week’s list.

If this is the case, the code will instead first generate the period variable. We use this as the title parameter for our enter_shopping_list_items function.

After that, I use more or less the same logic as in the update function to parse out the individual items. After calling the enter_shopping_list the first column of our shopping list tab is complete.

else:    
                    period =  f"Shopping list for week from Thursday {week.thursday()} to Wednesday {week_plus1.wednesday()}"  
                    for key, value in shopping_list.items():                
                       
                        if st.session_state[key] != '':
                            items = st.session_state[key].split(",")
                            for item in items:
                                item = item.strip()
                                shopping_list[key]['items'].append(item)                
                        db.enter_shopping_list_items(week_number, period, shopping_list)

The second column will hold all the items we want to buy this week. Clicking on an item will remove it from this week’s list. I also put in place a quick check to see if there are any items on this week’s list to avoid any pesky errors.

To make this work, we iterate over all the items in the current week’s list if it exists. If so, we create a button for each of them.

👉 Recommended: Streamlit Button – Ultimate Guide

Next, I assign a key parameter to them that needs to be unique to avoid the Streamlit app crashing. The easiest way I found to get this to work is to concatenate a unique value to the item name using the uuid module.

After that remove_item_shopping_list gets called on clicking the button. Because the app will reload after the click, the item will disappear from the column afterward.

with col2:
       
        current_shopping_list = db.get_shopping_list(week_number)
       
        if current_shopping_list:
            st.caption("Click an item to remove it from this weeks list")
           
            for k, value in current_shopping_list["shopping_list"].items():
                for item in value["items"]:
                    st.button(label = item, key =f"{item}{str(uuid.uuid4())[:8]}", on_click=db.remove_item_shopping_list, args= (str(week_number), k, item))
               
        else:
            st.subheader(f"You have not created a shopping list yet for week from Thursday {week.thursday()} to Wednesday {week_plus1.wednesday()}")

Step 10: Building the recipes tab

The recipes tab is built in more or less the same way as the shopping list tab. I start with the if-statement again to check if that is the button clicked. After that, I define the three columns we’ll need for this tab.

#---RECIPES TAB---#            
if nav_menu == "Weekly recipes":
    col1, col2, col3 = st.columns([4,4,2], gap = "medium")

After this setup, we can start to flesh out the first column. First, I check if there are any recipes currently active. This means that they are assigned to the first column. If there are no recipes active, I display a nice message for the user.

If, however, there are recipes active, I iterate through them.

Next, I used the Streamlit expander module and some additional layout tinkering to make it look nice. Using the expander module is like using the columns.

In the with-statement, we add the elements of the recipe by calling them by their respective keys. At the bottom, I insert two buttons. One to change the status of a recipe, thereby removing it from this column. The other will add the ingredients to this week’s shopping list. For the second button, I add the optional type = primary parameter to change its color.

with col1:
        st.subheader("This week's recipes:")  
       
        if len(db.get_recipe_status()) < 1:
            st.caption("This page will hold a clickable collection of recipes. \
                When you select one the idea is to both have it appear in a list on the first page and automatically add the ingredients to the shopping list.")
        else:
            st.caption("Click on a recipe to see the instructions or add the ingredients to the shopping list")
            for recipe in db.get_recipe_status():
                with st.expander(recipe["key"]):
                    "---"
                    for ingredient in recipe["ingredients"]:
                        st.write(ingredient)
                    '---'
                    for instruction in recipe["instructions"]:
                        st.write(instruction)
                    '---'
                    st.button(label = "Remove recipe from current week",key = f'{recipe["key"]}t', on_click=db.update_recipe_status, args=(recipe["key"],))
                    st.button(label = "Add ingredients to shopping list", key = f'{recipe["key"]}a', on_click=db.add_ingredients_to_shopping_list , args=(recipe["key"], str(week_number)),type = "primary")

The function of the second column is to give the user a place to enter their own recipes.

It uses an almost identical code to the shopping list item entry on our other tab. In essence, the form collects the entered recipe elements (name, ingredients, and instructions).

These elements are placed in the session_state variable as key-value pairs with the key I define for each text_area.

 with col2:
        st.subheader("Enter new recipe")
        with st.form("entry_form", clear_on_submit=True):
                       
            st.text_input("Recipe Name:", key="name")
            "---"
            st.caption("Please enter ingredient amounts, separated by commas")
            st.text_area("Ingredients: ", key = "ingredients")
            "---"
            st.caption("Please enter instructions, separated by commas")            
            st.text_area("Instructions: ", key = "instructions")
           
            submitted = st.form_submit_button("Save recipe", type = "primary")

As in the shopping list tab, we now also need to check if the submit button got clicked.

If so, we run the code in the if-statement to add the recipe elements to the recipe database item.

I first parse out every element from the session_state variable. Similar to shopping list items, I split and strip the ingredients and instructions. After parsing everything, I call enter_recipe to put it in recipe Base.

if submitted:
               
               
                recipe_name = st.session_state["name"].title()
                #---
                recipe_ingredients = st.session_state["ingredients"].split(",")
                recipe_ingredients = [f"- {i.strip()}" for i in recipe_ingredients]
                #---
                recipe_instructions = st.session_state["instructions"].split(",")
                for counter, instruction in enumerate(recipe_instructions, 1):
                    print(f"{counter}. {instruction.strip().capitalize()}")
                    recipe_instructions[counter-1] = f"{counter}. {instruction.strip().capitalize()}"
                #---
                db.enter_recipe(recipe_name, recipe_ingredients, recipe_instructions)

Putting together the code for the third and last column is very easy now. 

We put a nice subheader as a title and the caption to explain that clicking on a recipe adds it to this week’s recipe list.

Every recipe is a Streamlit button, generated by iterating over the not-active recipes. We concatenate an f to the end of each key variable to avoid conflicts with buttons in the first column.

 with col3:
        st.subheader("Recipe List: " )
        st.caption("Click a recipe to add it to this weeks list")
        for recipe in db.get_recipe_status("b"):
            st.button(label = recipe["key"],key = f'{recipe["key"]}f', on_click=db.update_recipe_status, args=(recipe["key"],))

Step 11: Putting it all together

If everything we did worked out well, the completed shopping_list.py should look like this.

#---PIP PACKAGES---#
import streamlit as st
from streamlit_option_menu import option_menu
from isoweek import Week


#---BUILT-IN PYTHON MODULES
from datetime import datetime, date
import calendar
from pprint import pprint
import uuid


#---IMPORT THE DATABASE PYTHON FILE db.py---#
import db as db


#---STREAMLIT SETTINGS---#
page_title = "Weekly dinner and shopping app"
page_icon = ":pouch:"
layout = "centered"


#---STREAMLIT PAGE CONFIG---#
st.set_page_config(page_title=page_title, page_icon=page_icon, layout=layout)
st.title(f"{page_title} {page_icon}")


#---STREAMLIT CONFIG HIDE---#
hide_st_style = """<style>
                #MainMenu {visibility : hidden;}
                footer {visibility : hidden;}
                header {visibility : hidden;}
                </style>
                """
st.markdown(hide_st_style, unsafe_allow_html=True)


#---PERIOD VALUES---#
year = datetime.today().year
month = datetime.today().month
day = datetime.today().day+4
months = list(calendar.month_name[1:])
week_number = date(year, month, day).isocalendar()[1]
week = Week(year, week_number)
week_plus1 = Week(year, week_number+1)


#---DICT AND LIST INIT---#
shopping_list = {
    "fruit_and_veg" : {"title" : "Fruit and Veggies", "items" : []},
    "meat_and_fish" : {"title" :"Fresh meat and fish", "items" : []},
    "housekeeping" : {"title" :"Housekeeping supplies", "items" : []},
    "carbs" : {"title" : "Potatoes, rice, pasta, etc", "items" : []},
    "snacks" : {"title" : "Snacks", "items" : []},
    "dairy" : {"title" : "Dairy", "items" : []},
    "personal_care" : {"title" : "Personal care", "items" : []},
    "pets" : {"title" : "Pets", "items" : []},
    "beverages" : {"title" : "Beverages", "items" : []},
    "spices_and_cond" : {"title" : "Spices and condiments", "items" : []},
    "frozen" : {"title" : "Frozen", "items" : []}
    }


key_dict = {
    'fruit_and_veg': [],
    'meat_and_fish': [],
    'housekeeping': [],
    'carbs': [],
    'snacks': [],
    'dairy': [],
    'personal_care': [],
    'pets': [],
    'beverages': [],
    'spices_and_cond': [],
    'frozen': []
    }


ingredients_dict = {
    }


instructions_dict = {
    }


#---NAV BARS---#
nav_menu = option_menu(
    menu_title = None,
    options = ["Current Week", "Weekly recipes"],
    icons = ["list-task", "cup-straw" ],
    orientation = "horizontal"
)


#---SHOPPING LIST TAB---#
if nav_menu == "Current Week":
    """code block for the shopping list tab"""
   
    st.header(f"Thursday {week.thursday()} to Wednesday {week_plus1.wednesday()}")
    col1, col2 = st.columns([4,4], gap = "medium")


    with col1:
        st.caption(f"Please enter items, separated by commas")
        with st.form("entry_form", clear_on_submit=True):
                       
            for k, value in shopping_list.items():
                st.text_input(f"{shopping_list[k]['title']}:", key=k)
            "---"
           
            submitted = st.form_submit_button("Save shopping list items", type = "primary")    
            if submitted:
                if db.get_shopping_list(week_number):
                   
                    for key in key_dict:
                        update_dict = {}
                        if st.session_state[key] != '':
                                items = st.session_state[key].split(",")
                                for item in items:
                                    item = item.strip()
                                update_dict[key] = items                  
                                db.update_shopping_list(str(week_number), update_dict)
                else:    
                    period =  f"Shopping list for week from Thursday {week.thursday()} to Wednesday {week_plus1.wednesday()}"  
                    for key, value in shopping_list.items():                
                       
                        if st.session_state[key] != '':
                            items = st.session_state[key].split(",")
                            for item in items:
                                item = item.strip()
                                shopping_list[key]['items'].append(item)                
                        db.enter_shopping_list_items(week_number, period, shopping_list)      


    with col2:
       
        current_shopping_list = db.get_shopping_list(week_number)
       
        if current_shopping_list:
            st.caption("Click an item to remove it from this weeks list")
           
            for k, value in current_shopping_list["shopping_list"].items():
                for item in value["items"]:
                    st.button(label = item, key =f"{item}{str(uuid.uuid4())[:8]}", on_click=db.remove_item_shopping_list, args= (str(week_number), k, item))
               
        else:
            st.subheader(f"You have not created a shopping list yet for week from Thursday {week.thursday()} to Wednesday {week_plus1.wednesday()}")


#---RECIPES TAB---#            
if nav_menu == "Weekly recipes":
    col1, col2, col3 = st.columns([4,4,2], gap = "medium")
   
   
    with col1:
        st.subheader("This week's recipes:")  
       
        if len(db.get_recipe_status()) < 1:
            st.caption("This page will hold a clickable collection of recipes. \
                When you select one the idea is to both have it appear in a list on the first page and automatically add the ingredients to the shopping list.")
        else:
            st.caption("Click on a recipe to see the instructions or add the ingredients to the shopping list")
            for recipe in db.get_recipe_status():
                with st.expander(recipe["key"]):
                    "---"
                    for ingredient in recipe["ingredients"]:
                        st.write(ingredient)
                    '---'
                    for instruction in recipe["instructions"]:
                        st.write(instruction)
                    '---'
                    st.button(label = "Remove recipe from current week",key = f'{recipe["key"]}t', on_click=db.update_recipe_status, args=(recipe["key"],))
                    st.button(label = "Add ingredients to shopping list", key = f'{recipe["key"]}a', on_click=db.add_ingredients_to_shopping_list , args=(recipe["key"], str(week_number)),type = "primary")
       
       
    with col2:
        st.subheader("Enter new recipe")
        with st.form("entry_form", clear_on_submit=True):
                       
            st.text_input("Recipe Name:", key="name")
            "---"
            st.caption("Please enter ingredient amounts, separated by commas")
            st.text_area("Ingredients: ", key = "ingredients")
            "---"
            st.caption("Please enter instructions, separated by commas")            
            st.text_area("Instructions: ", key = "instructions")
           
            submitted = st.form_submit_button("Save recipe", type = "primary")
           
            if submitted:
               
               
                recipe_name = st.session_state["name"].title()
                #---
                recipe_ingredients = st.session_state["ingredients"].split(",")
                recipe_ingredients = [f"- {i.strip()}" for i in recipe_ingredients]
                #---
                recipe_instructions = st.session_state["instructions"].split(",")
                for counter, instruction in enumerate(recipe_instructions, 1):
                    print(f"{counter}. {instruction.strip().capitalize()}")
                    recipe_instructions[counter-1] = f"{counter}. {instruction.strip().capitalize()}"
                #---
                db.enter_recipe(recipe_name, recipe_ingredients, recipe_instructions)
               
           
           
    with col3:
        st.subheader("Recipe List: " )
        st.caption("Click a recipe to add it to this weeks list")
        for recipe in db.get_recipe_status("b"):
            st.button(label = recipe["key"],key = f'{recipe["key"]}f', on_click=db.update_recipe_status, args=(recipe["key"],))

Running the app itself should result in the following views (minus the recipes themselves) for both of the tabs.

Conclusion

I hope you have been able to create your own shopping list and recipe app after reading this article. Or even better, make it more awesome! I’d be very interested to see the cool things you did with this.

You can find the Github for this project here.

Feel free to contact me with questions or suggestions 🙂