I use Trello for all kinds of personal stuff, one of which is keeping track of recipes and grocery lists. After planning which meals to cook and buy groceries for, a lot of manual work goes into checking every recipe and adding the ingredients to the grocery shopping list. Manual stuff is tedious, so I automated it with AWS Lambda. Read on to find out how!
The Trello board
NOTE: Our Trello board is in Swedish, so the screenshots will be in Swedish as well.
We have a Trello board that contains a lot of recipes grouped into lists, such as Pasta Dishes, Soups & Stews, and so on. We also have a list of groceries that needs to be bought, so that we always have the shopping list available when shopping.
In the above picture, you can get a glimpse of the Trello board, with each card containing a picture of the dish as well as a link to the external site where the recipe can be found.
What I wanted to automate was the following:
- Add a button to each card, that when clicked, adds the recipe to the Recipes to buy list (named Recept att handla in my board).
- Add a button to the board, that when clicked, reads through the Recipes to buy list and adds the ingredients (and amount of each) to the Grocery Shopping List (named AUTO Handlingslista in my board).
Below you can see the completed automation in action:
Automation setup
Trello part 1
I started by adding two new lists to my board, one to hold recipes that should be bought, and one for the generated grocery shopping list.
I then proceeded to add a checklist to recipe cards. This checklist contains the ingredients required for a single serving of the recipe. For example, if a recipe requires one onion for four servings, I would add onion:0.25
. Here I had to be consistent between recipes so that the units used for ingredients would be the same across all recipes.
The unit for onion here is number of onions, while for pasta I use gram and for cooking cream I use deciliter.
For ingredients where I do not care about the unit and just need to make sure that I buy it if needed, such as cooking oil and certain spices, I set the amount needed to 0
.
With the checklist(s) completed, I went on and created a Card button in the Automation menu. When clicked, this button triggers an action that adds a new card to the recipes to buy list. The created card gets the following title ANTAL_PORTIONER:{cardidlong}:{cardname}
. The ANTAL_PORTIONER
(number of servings in Swedish) is a placeholder that needs to be changed to an actual number before generating the shopping list.
The cards now look like this:
Clicking the button creates a new card in the recipes to buy list, and I then edit the text ANTAL_PORTIONER
in the title to the number of servings I need to purchase.
I also needed to get an API key and token for the Trello API, as described here.
I also needed to fetch the IDs of both the recipes to buy list, the grocery shopping list, and the board itself. This was easiest done by creating a new card in each of the lists, clicking on them and then adding .json
to the end of the URL in the browser. From there I could then extract the idList
and idBoard
variables.
AWS part
This whole thing really started with me wanting to find a use case for the newly released function URLs of AWS Lambda. So I decided to go with FastAPI in a Lambda function (as I usually do).
Since the API should be reachable from Trello I decided to keep the function URL open to the public. To secure it, I added code to the Lambda function that validates the X-Api-Key
header in incoming requests.
Secrets
To not require hard-coded secrets in the Lambda function, I added the following parameters to the AWS Systems Manager Parameter Store:
-
/trellomat/api_key
: Trello API Key -
/trellomat/token
: Trello token -
/trellomat/board_id
: ID of the Trello board -
/trellomat/shopping_list_id
: ID of the grocery shopping list -
/trellomat/recipe_list_id
: ID of the recipes to buy list -
/trellomat/aws_api_key
: The API key that is required in theX-Api-Key
header
Lambda code
Folder structure
The code examples below are structured as follows:
TrelloMatApi/
├── template.yml
├── api-function
│ ├── requirements.txt
│ ├── src
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── trello.py
│ │ ├── utils.py
requirements.txt
The Lambda function requires the following libraries.
mangum
fastapi
pydantic
py-trello
aws-lambda-powertools
__init__.py
The handler function is a FastAPI
application wrapped by a Mangum
adapter. The API includes a single route POST /generate
, that neither requires nor returns any payload.
Authorization is done with the X-Api-Key
header, which is validated by the auth
method. The valid key is fetched from AWS Systems Manager Parameter Store and cached between invocations.
from fastapi import FastAPI, HTTPException, Header, Depends
from mangum import Mangum
from aws_lambda_powertools.utilities import parameters
from src.utils import logger, tracer
from src import trello
app = FastAPI()
API_KEY = parameters.get_parameter("/trellomat/aws_api_key")
async def auth(x_api_key: str = Header(...)):
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Unauthorized")
@app.post("/generate", status_code=204, dependencies=[Depends(auth)])
def generate():
trello.generate()
handler = Mangum(app)
handler.__name__ = "handler"
handler = tracer.capture_lambda_handler(handler)
handler = logger.inject_lambda_context(handler, clear_state=True)
utils.py
This is a simple file that sets up a logger and tracer using AWS Lambda Power Tools.
from aws_lambda_powertools import Logger, Tracer
logger: Logger = Logger()
tracer: Tracer = Tracer()
models.py
This is where I define my ingredients.
In the INGREDIENTS
dict, I specify ingredients their units as well as which category the ingredient is part of.
The Category
decides which label an ingredient gets, and it is also used for sorting the generated shopping list. The default values in the Ingredient
class are there to not break the API whenever an ingredient is added on the Trello side before adding it to the Lambda function itself.
from enum import Enum
from pydantic import BaseModel
class Category(Enum):
GREEN = 0
FRIDGE = 1
BREAD = 2
FROZEN = 3
DRY = 4
OTHER = 5
NONE = 6
class Ingredient(BaseModel):
name: str
unit: str = "NOUNIT"
cat: Category = Category.NONE
amount: int = 0
INGREDIENTS = {
"vitlök": { # garlic
"unit": "klyftor", # cloves
"cat": Category.GREEN,
},
"gul lök": { # onion
"unit": "st", # pcs
"cat": Category.GREEN,
},
"matlagningsgrädde": { # cooking cream
"unit": "dl",
"cat": Category.FRIDGE,
},
...
}
trello.py
This is where the magic happens. First, all required information is fetched from AWS Systems Manager Parameter Store.
We then initialize a TrelloClient
from the py-trello library, as well as instantiate objects for the board and lists.
The generate
function then:
- Archives all existing cards in the grocery shopping list.
- For each card in the recipes to buy list (which has the format
{servings}:{recipe_card_id}:{recipe_name}
), it fetches the card with therecipe_card_id
. - On this card, it looks for the checklist named
Ingredienser
. - For each checklist item, it adds the required ingredient amount to the
ingredients
dict. It also adds the ingredient unit and category from the model defined earlier. - The
ingredients
dict is then sorted by category. - For each ingredient in the
ingredients
dict, it creates a new card in the grocery shopping list.
from aws_lambda_powertools.utilities import parameters
from trello import TrelloClient
from src.utils import logger
from src.models import INGREDIENTS, Ingredient, Category
TOKEN = parameters.get_parameter("/trellomat/token")
API_KEY = parameters.get_parameter("/trellomat/api_key")
BOARD_ID = parameters.get_parameter("/trellomat/board_id")
RECIPE_LIST_ID = parameters.get_parameter("/trellomat/recipe_list_id")
SHOPPING_LIST_ID = parameters.get_parameter("/trellomat/shopping_list_id")
CHECKLIST_KEY = "Ingredienser"
client = TrelloClient(api_key=API_KEY, token=TOKEN)
board = client.get_board(board_id=BOARD_ID)
recipe_list = client.get_list(list_id=RECIPE_LIST_ID)
shopping_list = client.get_list(list_id=SHOPPING_LIST_ID)
labels = board.get_labels()
def _get_label(cat: Category) -> list:
try:
return [labels[cat.value]]
except IndexError:
return []
def _archive_old_cards():
logger.info("Clearing AUTO shopping list")
shopping_list.archive_all_cards()
def _get_ingredient_list() -> list[Ingredient]:
logger.info("Getting ingredients")
cards = {card.id: card for card in board.get_cards()}
ingredients = {}
for card in recipe_list.list_cards_iter():
servings, ref_card_id, name = card.name.split(":")
ref_card = cards[ref_card_id]
logger.info(f"Reading recipe for {name}")
card_ingredients = next(
(cl for cl in ref_card.checklists if cl.name == CHECKLIST_KEY), None
)
if not card_ingredients:
logger.warning(f"No checklist named {CHECKLIST_KEY} found on card {name}")
continue
for item in card_ingredients.items:
name, amount = item["name"].split(":")
if name not in ingredients:
ingredients[name] = Ingredient(name=name, **INGREDIENTS.get(name, {}))
ingredients[name].amount += int(servings) * float(amount)
# Return list of Ingredients sorted by category
return sorted(ingredients.values(), key=lambda item: item.cat.value)
def _create_cards(ingredients: list[Ingredient]):
for ingredient in ingredients:
if ingredient.amount == 0:
title = f"{ingredient.name}"
elif ingredient.amount == int(ingredient.amount):
title = f"{int(ingredient.amount)} {ingredient.unit} {ingredient.name}"
else:
title = f"{ingredient.amount:.2f} {ingredient.unit} {ingredient.name}"
logger.info("Adding card: %s", title)
shopping_list.add_card(title, labels=_get_label(ingredient.cat))
def generate():
_archive_old_cards()
ingredients = _get_ingredient_list()
_create_cards(ingredients)
logger.info("Grocery list generated successfully")
template.yml
I deployed the Lambda function with AWS SAM. The SAM template looks like the following:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Trello Mat API
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 256
Timeout: 180
Tracing: Active
FunctionUrlConfig:
AuthType: NONE
CodeUri: api-function
Handler: src.handler
Runtime: python3.9
Policies:
- SSMParameterReadPolicy:
ParameterName: "trellomat/aws_api_key"
- SSMParameterReadPolicy:
ParameterName: "trellomat/token"
- SSMParameterReadPolicy:
ParameterName: "trellomat/api_key"
- SSMParameterReadPolicy:
ParameterName: "trellomat/board_id"
- SSMParameterReadPolicy:
ParameterName: "trellomat/recipe_list_id"
- SSMParameterReadPolicy:
ParameterName: "trellomat/shopping_list_id"
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: TrelloMatAPI
Outputs:
FunctionUrl:
Description: URL of the Function
Value: !GetAtt ApiFunctionUrl.FunctionUrl
Trello part 2
With the API complete, I created a new Board button in the Automation menu. When clicked, a post request is sent to the Lambda function URL, with the API key specified in the X-Api-Key
header. The Lambda function then reads the recipes to buy list and generates a grocery shopping list.
Final result
Lo and behold, the final result! After adding a few recipes to the recipes to buy list, I can now automatically generate a grocery shopping list.
Have you built anything similar to aid you in your regular day-to-day tasks? Let me know in the comments!
Top comments (1)
Great idea and execution.