Well, I’m back again with some more exciting information. This time around, we’ve got the “GroceryList” schema and endpoints, with a few revisions to how I wrote some of the previous endpoints. Since that deals with information I’ve already covered, I’ll start with it.
The New Ingredient Query
While attempting to write the logic out for how the GroceryList schema would be read into the backend (more on this later), I quickly ran into an issue that I should have seen coming: the tacked-on “All Ingredients” part of the RecipeSchema was causing the schema to invalidate itself, because “All Ingredients” wasn’t actually a proper field. I toyed with this for a little while but ultimately felt that it was better to scrap it entirely; it didn’t feel like good programming practice to me to add a sort-of field to the end of a schema and then write a bunch of pre_load
checks to get rid of it. It’s much smoother to have the schema directly reflect the fields of the model; that way the program can automatically handle nested schemas (which I need for the GroceryList).
Of course, that begs the question: if the RecipeSchema doesn’t hold all the ingredients that are a part of that Recipe, how will we get them? I did a bit of reading up on API structuring, and ultimately chose to use queries on the “/ingredients” endpoint. The other option would be to add an endpoint to the recipe/lists themselves that returned their ingredients (i.e., “/recipes/5/ingredients”), and while I like the nested loop, I think that grouping the endpoints by what they return is a better idea.
In order to do so, I first modified the basic “GET” route for ingredients to make use of a new function: get_ingredient_by_params()
, which returned a list of ingredients based on the arguments passed in.
This keeps things nice and simple on the routes page. The actual function is a bit more complex.
Essentially, this method makes use of several long join()
statements from sqlalchemy in order to return the ingredients in a given recipe
or list
. I used a set()
in order to eliminate duplicates, but in retrospect I don’t think that was necessary; the design of the database should prevent that regardless. I also added a check to see if there was a list
and a recipe
argument, and to raise an error if so.
This is the format that I’m going to be using for all of my queries. I modified my recipe quaries to use the same logic, and when I add in users (that’s for the next post), I’m going to use a similar pattern to query the user’s lists.
Now, with that out of the way, let’s look at the GroceryList schema and endpoints.
The Schema
The base schema of the GroceryList is extremely simple, and makes use of SQLAlchemyAutoSchema
to create it’s fields:
More complex behavior, however, is needed when it comes to creating a new GroceryList through this schema. I wanted to be able to initialize a GroceryList with Recipes already in it, definied by their “id” attribute, and I wanted the option to include ingredents directly into the GroceryList from the beginning. After all, this is fundamentally supposed to represent a list of ingredients; the fact that Ingredients are not directly tied to GroceryLists shouldn’t change that.
Let’s start with the recipes first. If a list is going to be initalized with recipes, then I just want to be able to pass the id
of the recipe, and have the backend take care of the rest. This presents a slight problem, though, because the GroceryListSchema
expects more information than just an id. To solve this, I created a pre_load
function that takes the provided id
s and turns them into actual representations of the Recipe:
This function iterates through all provided recipes, and for each one it returns the full schema of the recipe. If the recipe is not included in the form “{‘id’: number}”, then it throws an error.
I thought this was all I needed, but not quite. I began to get DataIntegrityError
s when I tried to insert the new list in. Looking through the data made me realize that the RecipeSchema
was essentailly returning a new recipe object for the Recipe
s that were already in the database. Consequently, when it tried to insert this new/old recipe, it threw an error.
I solved this by modifying the RecipeSchema
to sync any validated recipe with it’s version in the database (provided one existed). This way, the program didn’t try to re-add the recipe.
This got rid of the error. Now, I had to add an ability to add ingredients directly to the RecipeList, without necessiarily having a Recipe act as an intermeditiary. In order to do so, I decided to return to my fix from the previous version: an “Additional Ingredients” Recipe model that is automatically added to every list, and which stores ingredients that the user directly adds to the list.
I initiated this list with another post_load
function in my GroceryListSchema.
This creates a new recipe called “Additional Ingredients” and iterates through all privided ingredients (which must be provided in a way that the IngredientSchema will understand). For each of the ingredients, it adds a RecipeLine to the Recipe, with the same text as the ingredient. This is essentially what happened in my last version of this app, although this time around, the data is much better organized.
Finally, because I had more than one post_load
function, I combined them into a single function to make the code easier to read and ensure that they would be run in the proper order.
Note that this function also checks if there is a “recipes” key in the data
, and creates one if not. This prevents the create_additional_ingredients
function from crashing.
With the schema built, it was time to add in my endpoints.
Endpoints
The “GET” and “POST” request for GroceryList objects are very simple and follow the same format as the other endpoints.
I have not yet made any direct filters for lists, primarly because I haven’t added in the User
functionality yet, and that will be the main filter. I may also add one for recipes, to return a list of GroceryLists that have a certain Recipe in them.
Otherwise, it’s all pretty straightforward. The GroceryListSchema does most of the heavy lifting of creating the object and validiting the code, so all I have to do is pass the json data to it and add the provided list.
Next, I added a “PUT” request to a specific grocery list. This one is slightly more complicated, and relies on a helper function, add_additional_ingredients
.
This function checks for Recipes and Ingredients, adding them one by one if they exist. Recipes must be added by ID. Ingredients requred the additional function, becuase I needed to load up the “Additional Ingredients” Recipe first, then add the ingredients as RecipeLines. Note that I load the Ingredients using an IngredientSchema. I do this so that it returns the Ingredient from the database (if it exists), or saves the new Ingredient to the database (if it’s a new ingredient). This prevents IntegrityError
s.
Again, Marshmallow has made this so much less complicated; I don’t have to worry about creating new Ingredients, because I already wrote that code last week and it doens’t need to be rewritten. It’s pretty nice.
Finally, I added a “DELETE” endpoint. This was slightly more complicated than it first sounds, only becuase I wanted to make sure I deleted the “Additional Ingredients” Recipe along with the GroceryList.
Conclusions
And with that, the GroceryList endpoints and schema are done. I just need to add the User
schema and endpoints and the bulk of the backend will be done. After that, I still need to add the scraper and integrate spaCy. Then, it’s time to start the frontend.