Sexy Pizza

Sexy Pizza - South Pearl, Denver

I took a 5K sunset hike in Morrison, CO, today, a solid 20 minutes beyond surburban Denver and into the foothills. Afterwards, I was starving for an uncomplicated meal, and my friend Shauna has been after me to try Sexy Pizza for some time. It was right on the way home, so I stopped in. Pearl Street was unusually quiet so late at night, and I sat near the street in front of the wide-open garage door. An aside - it’s very awkward to walk into a restaurant that has a normal door adjacent to an open garage door. Which is the right entrance?

It was nice seeing all the passersby, and the service staff was, like the restaurant, uncomplicated. They let me walk into the back unannounced to get a box at the end of the meal, they didn’t bother me needlessly once my order was placed, and carried on lighthearted, boisterous conversation.

I can’t express the same happiness for their product. Thick dough goes into a very hot brick-style oven, and the bottom of the crust takes on a Neapolitan crisp. The top of the dough stayed pillowy, however, and it made for a weird texture - too crunchy on the bottom, but underdone near the toppings. I do confess I ordered a lot of watery toppings in the form of a veggie pie, but the weird duality of the texture turned me off. My goal for my time in Denver is to not eat dinner at the same place twice - here’s one I won’t regret not visiting again.

Sing a Song for Sixpence

I’ve been wanting to do this for some time, and I think I’m taking the plunge - you’ll start seeing restaurant reviews on here as well as code.

Four and Twenty, East Johnson, Madison

To start with, I can’t believe I’ve never been here. Well, actually, it stands to reason - Four and Twenty stands in a tiny commercial space amid tightly-knit ranch houses and sandwiched between the thoroughfares of East Wash and Packers Ave, so it’s easy to drive right on past and not give it a second thought.

To do so would be a mistake. Mel and I were on our way to an art fair in the neighborhood and she commented that her friend Phil sometimes works here. We were looking for something small and quick to eat, and Four and Twenty fit the bill.

The cafe is small with big windows out onto a soccer field and a concrete patio with planters - in summer it’s flooded with light, which illuminates all the imperfections (and the artwork) inside. The swinging doors to the kitchen don’t completely block its view, nor that of the supply room, giving it a just-moved in feel.

Mel and I ordered breakfast sandwiches, and they were probably my ideal quick brunch if I could ever describe it. I had an egg and cheddar with spinach and sauteed mushrooms, on the second-best biscuit I’ve ever had (that honor goes to Third & Hollywood in Columbus). The biscuit was big and firm. It didn’t flake off in layers or crumble in little ball-bearings of dough - it held together and didn’t stick my tongue to the roof of my mouth. This biscuit is definitely something to write home about. A small cup of fruit accompanied the sandwich and made for a well-priced, well-portioned breakfast that didn’t make me want to nap afterward.

I should also mention my drink, since it was even better than my sandwich - props to Phil. An iced chai latte with a rich milkiness that I’m often lacking. It was understated and sweet and I’d like to have another soon.

A Homegrown Recipe App That Helps You Cook

I do love to cook. All of my favorite recipes are stored in my head, are improvised off the cuff, or found in a gross stack of crinkled papers held together with a binder clip in my cabinet.

I do a lot of looking up recipes online, but every recipe website I’ve seen has a big flaw. These sites are not designed to help you actually read the recipe. They are designed to keep you clicking around, keep you sharing recipes on social media, keep you seeing ads.

I wanted a recipe app that would just show me what I needed to gather and how to put it together to cook it all. In the meantime, I decided it wouldn’t hurt it if were also pretty to look at. Lastly, I wanted it optimized for use on an iPad so that I could keep it close at hand while actually cooking.

Finished Recipes Webapp

Technical Design

Python is my pet language, and I wanted a database that would be easy to use with native Python types. Recipes are well-suited to a document-oriented store - each recipe has highly-variable ingredients. The only potential advantage I could see for a relational db would be to answer queries like “show me all the recipes with onion as an ingredient”. RethinkDB is great because it has a native web interface and a cool query language called ReQL.

I used Python and Flask to write the app, and Tornado to make it easy to deploy - I can never seem to get uWSGI or Gunicorn working correctly.

Frontend Design

Two design elements set Rethink Recipes apart:

  1. When a recipe is entered, the app finds the top 8 Google Images hits for the title of the recipe. It uses the colors found in these images to calculate a unique, indivualized color palette for each recipe.

  2. Whenever an ingredient appears verbatim in the directions, it is highlighted in the directions. This makes it easy to scan the directions for the step you should be on next.

App Routes

Here are the nuts and bolts of how Rethink Recipes works:

Adding a Recipe

Adding a Recipe

When accessing /add, the user is presented with 3 form fields - the title, ingredients, and directions.

When the form is submitted, a bunch of things happen:

  1. The title of the recipe is joined with + signs. i.e. “Foo bar baz pancakes” -> “foo+bar+baz+pancakes”. This is the format the Google Images api receives queries in.
  2. A Google Image API query returns a list of URLs pointing to Google Images thumbnails.
  3. A Python module called Colorific, which uses PIL (the Python Image Library), extracts the most prominent colors from each image, 5 colors per image. (I also wrote my own module to do this processing, but Colorific was already packaged and easy to install. )
  4. We’re left with a list of RGB tuples. 5 colors per images times 8 images = 40 tuples long.
  5. The Lightness value of each tuple is calculated (conversion to HSV color space), and the list is reordered in order of darkest to lightest.
  6. The list is split into 4 smaller lists of 10 elements each. This roughly groups the colors into the 10 darkest, 10 next darkest, etc.
  7. The average value of the 10 RGB tuples in each list is calculated. We end up with 4 tuples to store to the db along with the recipe.
  8. The URL title is “slugified” - stripping out spaces and non alphanumeric punctuation to make a nice URL slug
  9. The ingredients list is calculated. For each line in the ingredients box:
    • Split on spaces. The first 2 pieces are the amount - “1 cup” for example.
    • Everything after the amount is the “what”
  10. The directions list is each line of input in the dirctions box
  11. All this data is formed into a Python dictionary:
    • Title, URLs of image thumbs, ingredients, directions, slug, and average colors.
  12. Ingredients and directions are sanitized for disallowed entries (like null string)
  13. This Python dict is inserted into RethinkDB.
  14. The user is redirected to the newly-created recipe.
def get_gimages(query):
    answer = []
    for i in range(0,8,4):
        url = 'https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=%s&start=%d&imgsz=large&imgtype=photo' % (query,i)
        r = urllib2.urlopen(url)
        for k in json.loads(r.read())['responseData']['results']:
             answer.append(k['tbUrl'])
    return answer


@app.route('/add', methods = ['GET', 'POST'])
def add():
    form = RecipeForm()
    #form.ingredients.data = "1c ingredient\nenter ingredients here  "

    if request.method == 'POST' and form.validate():

        lightness = []
        rainbow = []
        resp=[]
        urls = get_gimages("+".join(form.title.data.split()))
        for url in urls:
            i = cStringIO.StringIO(urllib.urlopen(url).read())
            quiche = colorific.extract_colors(i, max_colors=5)
            resp.extend([each.value for each in quiche.colors])
        resp = [(m,sqrt(0.299 * m[0]**2 + 0.587 * m[1]**2 + 0.114 * m[2]**2)) for m in resp]
        lightness = sorted(resp,key=lambda x: x[1])
        lightness = [i[0] for i in lightness]
        lightness.sort(key=lambda tup: colorsys.rgb_to_hsv(tup[0],tup[1],tup[2])[2])
        for each in chunks(lightness,10):
            avg = tuple(map(lambda y: sum(y) / len(y), zip(*each)))
            rainbow.append(avg)

        slug = slugify(form.title.data)

        recipe = { 'title': form.title.data.title(), 'ingredients': [{'amount': " ".join(ingredient.split()[0:2]), 'what': " ".join(ingredient.split()[2:])} for ingredient in form.ingredients.data.split('\r\n')], 'directions': form.directions.data.split('\r\n'), 'urls': urls, 'slug': slug, 'avgcolors': [list(i) for i in rainbow]}

        recipe['ingredients'] = [i for i in recipe['ingredients'] if not i['what']=='']
        recipe['directions'] = [i for i in recipe['directions'] if not i=='']

        r.table('recipes').insert(recipe).run(g.rdb_conn)

        return redirect(url_for('recipe', query=slug))

    return render_template("add.html", form=form)

Editing a Recipe

The same form from /add is rendered, with the data from the existing recipe filled in. Once the form is submitted, the same process as adding happens. Instead of inserting the new recipe, however, the existing recipe’s record is updated.

The slug of the recipe appears in the URL of the page. We use that slug to get the first recipe in the db that matches that slug. Note that there’s no guarantee of uniqueness for a slug - RethinkDB doesn’t have unique indexing.

@app.route('/<query>/edit', methods = ['GET', 'POST'])
def edit(query):


    form = RecipeForm()
    recipe = list(r.table('recipes').filter({'slug': query}).run(g.rdb_conn))

    if recipe:
        recipe = recipe[0]
    else:
        abort(404)

    if request.method == 'GET':

        form.ingredients.data = "\r\n".join([(i['amount'] + " " + i['what']) for i in recipe['ingredients']])
        form.directions.data = "\r\n".join(i for i in recipe['directions'])
        form.title.data = recipe['title']

        return render_template("edit.html", form=form, recipe=recipe)
        id = recipe['id']

        r.table('recipes').get(id).update(recipe).run(g.rdb_conn)

        return redirect(url_for('recipe', query=slug))

Deleting a Recipe

Deleting a Recipe

The recipe’s slug is in the URL. The user clicks “Delete”, is prompted with a checkbox to confirm, and then recipes with slugs matching the URL are deleted:

@app.route('/<query>/delete', methods = ['GET', 'POST'])
def delete(query):
    form = DeleteForm()
    recipe = list(r.table('recipes').filter({'slug': query}).run(g.rdb_conn))
    if recipe:
        recipe = recipe[0]
    else:
        abort(404)

    if request.method == 'GET':

        return render_template("delete.html", query=query, recipe=recipe, form=form)

    if request.method == 'POST':
        if 'deleterecipe' in request.form:

            id = recipe['id']

            r.table('recipes').get(id).delete().run(g.rdb_conn)
            return redirect(url_for('index'))
        else:
            flash("Are you sure? Check the box")
            return render_template("delete.html", query=query, recipe=recipe, form=form)

Displaying a Recipe

When a recipe’s slug appears alone in the URL, the recipe is displayed.

The first recipe in the db with a matching slug is displayed. We take the “what” from each ingredient, and only take the portion before a comma. This allows for things like “onions, diced” to appear in the recipe, but still highlight the word “onions” in the directions. If the word appears in the directions, then the word in the directions is wrapped in kbd tags, which stand out in Bootstrap.

You’ll recall that each recipe has 8 URLs of Google images and 4 RBG tuples of colors to use in displaying the page. When the page is rendered, the darkest of the RGB colors is used as the background of the header, the lightest is used for the page background and header text, and the 2nd darkest is used for the panel headers. The 4th one is reserved for future use.

Along with the 8 Google images thumbnails in place, this makes each recipe page have the unique colors corresponding to the food you’re making! So a raspberry pie has bright reds and rich tans, and a chili recipe has lots of earthy tones.

@app.route('/<query>')
def recipe(query):
    recipe = list(r.table('recipes').filter({'slug': query}).run(g.rdb_conn))

    if recipe:
        recipe = recipe[0]
    else:
        abort(404)

    steps = []
    for i in recipe['directions']:
        for j in [k['what'].split(",")[0] for k in recipe['ingredients']]:
            i = i.replace(j, ("<kbd>" + j + "</kbd>"))
        steps.append(Markup(i))

    rainbow = [tuple(l) for l in recipe['avgcolors']]

    return render_template("recipes.html", urls = recipe['urls'], avg=rainbow, ingredients = recipe['ingredients'], steps = steps, title=recipe['title'], query=query)

The end result of this is an iPad-optimized recipe database, customizable for your own personal recipes. Not great for importing directly from recipe sites, but great for saving your time-tested recipes in a user-friendly format.

Futures

I would like to expand this to being multi-user. I think each user would get their own RethinkDB table, and add a users table with hashed passwords.

Check It Out

You can download and set it up for yourself in a virtual machine - see the Github repo for the project

Delicious Vegan Biscuits and Gravy

Next up, a departure from my usual programming entries to another hobby - cooking.

It’s Veganuary! It’s a month of sacrifice for me, in the name of shedding holiday pounds.

The single hardest thing for me about going vegan is finding quick meals to make that aren’t a piece of fruit.

I think it’s similar to when I stopped eating meat. At first, I started cooking the same old meals, just with the meat removed and a vegetable or mushrooms substituted. These sometimes felt lacking. Over time, I learned to cook “natively veggie” meals - meals that were vegetarian from the start, whose flavors melded and combined deliciously without the need for meat.

Now that I’m not eating dairy or cheese, I find my usual meals lacking - because there’s no dairy or cheese. It will take some time to learn to cook “natively vegan”, I’m sure.

In the meantime, however, here’s an amazing, hearty, breakfast, that’s totally vegan.

Vegan Biscuits and Gravy

Serves 4

Ingredients

  • 12 oz Trader Joe’s Soy Chorizo
  • 1 medium yellow onion, diced
  • 1 green bell pepper, diced
  • 2 oz Earth Balance buttery vegan spread
  • 2 oz whole wheat flour
  • 1 Tbsp Better than Bouillon No-Beef Base
  • Garlic Naan or vegan english muffins

Directions

  1. With a little olive oil, brown the chorizo, onions, and peppers in a large skillet. Once browned, move the mixture to one half of the pan.
  2. Melt the earth balance in the empty half of the pan. Add the flour and whisk lightly until a pasty consistency is achieved. Allow this roux to brown for a minute.
  3. Mix bouillon and 1 cup of water. Add to the roux a little bit at a time, whisking constantly until gravy-like. Mix the chorizo mixture in with the broth. Add water to thin to your liking.
  4. Toast the garlic naan or english muffin. Slice into thin wafers and top the bread with biscuits and gravy. Yum!

Vegan Biscuits and Gravy

LibUSB and Python to Overcome Bad USB Design

Last October, I began recording temperatures for my homebrew setup. It used a C program called pcsensor that someone reverse-engineered from the Windows version available on the CD that came with my TEMPer1v1.4 sensors.

The problem with these sensors is that they have no unique identifier - reading the value in from pcsensor with multiple sensors installed often resulted in the data series being mixed together, because there was no “order” to the way data was being read into the parser.

Here’s a demonstration of what I mean:

Graph Interference

Notice how the order of the orange and red points switch a lot? This made it really difficult to analyze the temperatures in my setup, apart from using a human to look at the trendline created by the mix of points.

I spent some time trying to create a driver for these TEMPer sensors, but in the end I was able to find one by Phillip Adelt that uses libusb and pyusb - libraries that allow you to talk to USB devices with Python.

git clone https://github.com/padelt/temper-python.git
cd temper-python
sudo python setup.py install

The ingenius thing that Phillip did was to determine the port and bus of each sensor - thus giving it a “unique ID” based on where it was physically plugged into the computer!

His module installs itself as “temperusb”, so I wrote a quick script to read the value of the sensors:

#!/usr/bin/python
# encoding: utf-8

from temperusb import TemperHandler
import json

th = TemperHandler()
devs = th.get_devices()


for i, dev in enumerate(devs):
    temps = dev.get_temperature(format="fahrenheit")
    ports = dev.get_ports()
    bus = dev.get_bus()
    print (bus, ports, temps)

Linux limits which users can access raw USB data, so we either need to install some udev rules or run the script as superuser. Sudo is easiest for me. It outputs something like:

fred@brewery:~$ sudo python temper_read.py
[sudo] password for fred:
(2, 2, 56.8625)
(2, 1, 22.1)
fred@brewery:~$

The bus and port will be consistent, even if the order of the entries gets switched. Now we can depend on the results of these sensors, and send them to InfluxDB:

#!/usr/bin/python
# encoding: utf-8

from temperusb import TemperHandler
import json
import socket
hostname = socket.gethostname()

th = TemperHandler()
devs = th.get_devices()

from influxdb import InfluxDBClient
client = InfluxDBClient('hostname', 8086, 'root', 'root', 'temperatures')

for i, dev in enumerate(devs):
    d =[]
    ports = dev.get_ports()
    bus = dev.get_bus()
    d.append({"points":[[i, dev.get_temperature(format="fahrenheit"), ports, bus]], "name": "%s_temper_%s_%s" % (hostname,ports, bus), "columns": ["index", "temperature", "port", "bus"]})
    try:
        client.write_points(d)
    except:
        raise Exception("Couldn't connect to influxdb")
        print "Couldn't connect to InfluxDB"
        print json.dumps(d,indent=2)

I put this in the superuser’s crontab:

sudo crontab -e
# m h  dom mon dow   command
* * * * * /usr/bin/python /home/fred/temper_influx.py

This sends the available temperatures to InfluxDB once a minute.

Which leaves me very happy with my temperature-monitoring setup:

Grafana Temperatures Dashboard