Wednesday 12 December 2012

Programming In Python 1: The Cookbook Project

Finally it's finished! Well as finished as it'll ever be. Almost a year a go I started learning Python, the programming language on and off. Which really isn't an ideal way to learn.

As with anything we want to learn in life I needed source materials, examples and exercises to complete. I found these in the Ubuntu orientated on-line magazine Full Circle. One of the early tutorials dives straight into writing a simple program that creates, loads and updates a database of cooking recipes.

The data in the database in this exercise isn't particularly important. It's the lessons learnt in writing the program that creates and manipulates the database. And fortunately Python is a very rich language with a lot of additional modules that can be called upon to enhance the power of software written in Python.

Personally I don't think what I've produced is particularly impressive. But it's where I'm at. For example I really should be making better use of Python's class objects. I guess I still have some habits from learning COMAL and Turbo Pascal bouncing around in my brain getting in the way. I guess I can only get better right?

With some basic concepts learnt, the next challenge is to create a program that will win me the lottery. On the off chance anybody is interested I've included my source code below.

#!/usr/bin/env python
#------------------------------------------------------------------------------
#
#    Program Title    :    Cookbook Database
#    Local Filename    :    sql-0004-cookbook_database.py
#    Author            :    Kevin Lynch
#    Created            :    04.06.2012
#
#------------------------------------------------------------------------------
#
#    Brief:    Write a program capable of storing recipes for later retrival.
#
#                * The program should be menu driven.
#                * Include a search function allowing users to search by;
#                    + Recipe title,
#                    + Author,
#                    + Ingredients.
#
#                * The program should also be capable of creating new cookbooks.
#                * Adding new recipe entries to each relevant book on demand.
#                * Removing unwanted entries from a book on demand.
#                * Program output and interaction prompts must be presentable.
#
#------------------------------------------------------------------------------
#
#    Additional Credits:
#
#        This project is based on the Python tutorials published by
#        Full Circle Magazine. So far as this author can tell the original
#        "cookbook" tutorial was written by Greg Walters of
#        "RainyDay Solutions, LLC" and "www.thedesignatedgeek.com".
#
#        Terminal dimensions code courtisy of Grant Edwards.
#        http://bytes.com/topic/python/answers/607757-getting-terminal-display-size
#
#------------------------------------------------------------------------------
#--|    Import Modules    |----------------------------------------------------

import os
import apsw # SQLite wrapper.
import string
#import webbrowser
import termios, fcntl, struct, sys

#------------------------------------------------------------------------------
#--|    Class/Object Definition Section    |-----------------------------------
class aScreen():
    def __init__(self,aTitle,aBorder,aJustify,aMessage,aContent,aOptionlist):
#        Get Dimentions
        s = struct.pack("HHHH", 0, 0, 0, 0)
        fd_stdout = sys.stdout.fileno()
        x = fcntl.ioctl(fd_stdout, termios.TIOCGWINSZ, s)
#        print '(rows, cols, x pixels, y pixels) =',
#        print struct.unpack("HHHH", x)
#        return struct.unpack("HHHH", x)
        s = struct.unpack("HHHH", x)

        self.iDefault = 'Press "Q" to quit <::> '

        self.H = (s[0] - 6)
        self.W = s[1]
        self.J = aJustify # Tells the aScreen object which type of justification to use.
        self.T = aTitle
        self.B = (aBorder * self.W)
        self.P = 1
        self.M = aMessage # Should be a short single line instruction.
        self.C = aContent # Should be a list. Each list entry will correspond to a line in the terminal.
        self.O = aOptionlist # List of valid responses for this screen.

    def checkLine(line):
        pass

    def formatScreen(self):    # Formats the screen output. Works for showScreen() and should not be called directly.
        # Initialise screen segmentation.
        seg1 = [self.T,self.B,' ']
        seg2 = []
        seg3 = [' ',self.B]
        lines = []

        # Copy charcaters one at a time from self.C to creat a string of a maximum length of self.W - self.P
        if len(self.C) < self.H:
            j = len(self.C)
        else:
            j = self.H

        for i in range(0,j):
            line = self.C[i]
            if len(line) < self.W:
                seg2.append(' %s' % line)
            else:
                j = (self.W - self.P)
                while len(line) >= self.W:
                    if line[j] == ' ':
                        seg2.append(' %s' % line[0:j])   
                        line = line[(j + 1):len(line)]
                        i += 1
                    else:
                        j -= 1
                seg2.append(' %s' % line)
                i += 1

        # Add filler lines.
        if len(seg2) < self.H:
            for i in range(len(seg2),self.H):
                seg2.append(' ')

        # Add everything to one big list.
        for i in range(0,len(seg1)):
            lines.append(seg1[i])
        for i in range(0,len(seg2)):
            lines.append(seg2[i])
        for i in range(0,len(seg3)):
            lines.append(seg3[i])
        return lines

    def errorScreen(self):
        pass

    def showScreen(self): # Displays the current screen.
        loop = True
        lines = self.formatScreen()
        while loop == True:
            # Print the title, main body and borders of the screen.
            for i in range(0,len(lines)):
                print lines[i]
            # Prompt the user for input from the keyboard and verify the response.
            kbd = raw_input(self.M)
            for i in range(0,len(self.O)):
                if kbd == self.O[i]:
                    loop = False
                elif self.O[0] == 'pass':
                    loop = False
                else:
                    self.errorScreen() # When an invalid option is made the user is told.
        return kbd

class newRecord():
    def __init__(self): # Initialise the new recipe class.
        # Variables for Recipes table.
        self.name = ''
        self.servings = 0
        self.source = ''
        # Variables for Instructions table.
        self.instructions = ''
        # Variables for Ingredients table.
        self.ingredients = []
        # General variables needed for this record.
        self.recID = 0

class newDB():
    def __init__(self,dbname): # Initialise the Cookbook class.
        global connection
        global cursor
        self.totalcount = 0
        connection = apsw.Connection(dbname)
#        connection = apsw.Connection("cookbook1.db3")
        cursor = connection.cursor()

    def addRec(self,rec):
        sql = 'INSERT INTO Recipes (name,servings,source) VALUES ("%s",%s,"%s")' % (rec.name,str(rec.servings),rec.source)
        cursor.execute(sql)
        sql = "SELECT last_insert_rowid()"
        cursor.execute(sql)
        for x in cursor.execute(sql):
            rec.recID = x[0]
        sql = 'INSERT INTO Instructions (recipeID,instructions) VALUES (%s,"%s")' % (rec.recID,rec.instructions)
        cursor.execute(sql)
        for x in range(0,(len(rec.ingredients) - 1)):
            sql = 'INSERT INTO Ingredients (recipeID,ingredients) VALUES (%s,"%s")' % (rec.recID,rec.ingredients[x])
            cursor.execute(sql)

    def deleteRec(self,rid):
        sql = "DELETE FROM Recipes WHERE pkID = %s" % rid
        cursor.execute(sql)
        sql = "DELETE FROM Instructions WHERE recipeID = %s" % rid
        cursor.execute(sql)
        sql = "DELETE FROM Ingredients WHERE recipeID = %s" % rid
        cursor.execute(sql)
       
    def listAll(self): # Create a list of all recipes.
        res = ['%s %s %s %s' % ('Item'.rjust(6),'Name'.ljust(30),'Serves'.ljust(7),'Source'.ljust(30))]
        for x in cursor.execute('SELECT * FROM Recipes'):
            res.append('%s %s %s %s' % (str(x[0]).rjust(6),x[1].ljust(30),x[2].ljust(7),x[3].ljust(30)))
        return res

    def listOne(self,rec):
        sql = 'SELECT * FROM Recipes WHERE pkID = %d' % rec.recID
        for x in cursor.execute(sql):
            rec.recID = x[0]
            rec.name = x[1]
            rec.servings = x[2]
            rec.source = x[3]
        sql = 'SELECT * FROM Ingredients WHERE RecipeID = %s' % rec.recID
        for x in cursor.execute(sql):
            rec.ingredients.append(x[1])
        sql = 'SELECT * FROM Instructions WHERE RecipeID = %s' % rec.recID
        for x in cursor.execute(sql):
            rec.instructions = x[1]
        return rec

    def searchDB(self,sql,option):
        try:
            if option != '3':
                # Do search for options 1 and 2
                res = ['%s %s %s %s' % ('Item'.ljust(5),'Name'.ljust(30),'Serves'.ljust(6),'Source'.ljust(30))]
                for x in cursor.execute(sql):
                    res.append('%s %s %s %s' % (str(x[0]).rjust(5),x[1].ljust(30),x[3].ljust(20),x[2].ljust(30)))                   
            else:
                # Do search for option 3
                res = ['%s %s %s %s %s' % ('Item'.rjust(5),'Name'.ljust(30),'Serves'.ljust(6),'Source'.ljust(25),'Ingredient'.ljust(50))]
                for x in cursor.execute(sql):
                    res.append('%s %s %s %s %s' % (str(x[0]).rjust(5),x[1].ljust(30),x[2].ljust(6),x[3].ljust(25),x[4].ljust(50)))
        except:
            # Catch exception.
            res ['I have encountered a problem performing your search request!']

        return res
#------------------------------------------------------------------------------
#--|    Function Definition Section    |---------------------------------------
def LAR(db,title,message): # List all recipes in the database.
    c = db.listAll()
    lars = aScreen(title,'*','',message,c,['pass'])
    kbd = lars.showScreen()
    return kbd

def SAR(db,kbd): #Show a single recipe.
    # Show list of recipes to select from.
    if kbd == 'pass':
        kbd = LAR(db,'The Cookbook Project > Select A Recipe','Press "Q" to quit or make a selection <: br="br">    # Retrieve the choosen recipe from the database.
    elif kbd.isdigit() == True:
        rec = newRecord()
        rec.recID = int(kbd)
        rec = db.listOne(rec)
        # Display the results.
        c = [rec.name,' ','Preperation Steps:',rec.instructions,' ','Ingredients:']
        for x in range(0,len(rec.ingredients)):
            c.append(rec.ingredients[x])
        c.append(' ')
        c.append('Serves: %s' % str(rec.servings))
        c.append(' ')
        c.append('Written by %s' % rec.source)
        sars = aScreen('The Cookbook Project','*','','Press any key to continue <: br="br" c="c" pass="pass">        kbd = sars.showScreen()
    elif kbd == 'delete':
        kbd = LAR(db,'The Cookbook Project > Select A Recipe For DELETION!','Press "Q" to quit or make a selection <: br="br">        return kbd

def SRD(db): # Search for a recipe.
    # Determine search criteria.
    SRDS = aScreen('Search Database','*','pass','Press "Q" to quit or make a selection :> ',['Search by ...','1 - Recipe Name','2 - Author','3 - Ingredients'],['1','2','3','Q'])
    SRDSa = aScreen('Search Database By Recipe Name','*','pass',':> ',['What is the name of the recipie you would like to search for?'],['pass'])   
    SRDSb = aScreen('Search Database By Author','*','pass',':> ',['Who would you like to search for?'],['pass'])   
    SRDSc = aScreen('Search Database By Ingredients','*','pass',':> ',['Which ingredients would you like to search for?'],['pass'])   
    kbd = SRDS.showScreen()
    rec = newRecord()
    if kbd != 'Q':
        if kbd != '3':
            if kbd == '1':
                kbd = SRDSa.showScreen()
                sql = "SELECT pkID,name,source,servings FROM Recipes WHERE name LIKE '%%%s%%'" % kbd
                res = db.searchDB(sql,'pass')
            else:
                kbd = SRDSa.showScreen()
                sql = "SELECT pkID,name,source,servings FROM Recipes WHERE source LIKE '%%%s%%'" % kbd
                res = db.searchDB(sql,'pass')
        else:
            kbd = SRDSa.showScreen()
            sql = "SELECT r.pkID,r.name,r.servings,r.source,i.ingredients FROM Recipes r LEFT JOIN Ingredients i ON (r.pkID == i.recipeID) WHERE i.ingredients LIKE '%%%s%%' GROUP BY r.pkID" % kbd
            res = db.searchDB(sql,'3')

        SRDS = aScreen('Search Results','*','pass','Press "Q" to quit or make a selection :> ',res,['pass'])
        kbd = SRDS.showScreen()
        SAR(db,kbd)

def ARD(db): #Add a recipe to the database.
    nr = newRecord()
    screen = aScreen('Add A New Recipe','*','pass','Please enter the title of your recipe or press "Q" to quit :> ',[''],['pass'])
    nr.name = screen.showScreen()
    screen = aScreen('Add A New Recipe','*','pass','Please enter how many your recipe serves or press "Q" to quit :> ',[nr.name],['pass'])
    nr.servings = screen.showScreen()
    screen = aScreen('Add A New Recipe','*','pass','Please enter the name of the author of your recipe or press "Q" to quit :> ',[nr.name,'Serves %s' % nr.servings],['pass'])
    nr.source = screen.showScreen()
    screen = aScreen('Add A New Recipe','*','pass','Please enter the ingredients for your recipe or press "N" to move on :> ',[nr.name,'Serves %s' % nr.servings,'Written by %s' % nr.source],['pass'])
    kbd = screen.showScreen()
    loop = True
    while loop == True:
        if kbd == 'N':
            loop = False
        else:
            nr.ingredients.append(kbd)
            screen = aScreen('Add A New Recipe','*','pass','Please enter the ingredients for your recipe or press "N" to move on :> ',[nr.name,'Serves %s' % nr.servings,'Written by %s' % nr.source,' ','Ingredients'] + nr.ingredients,['pass'])
            kbd = screen.showScreen()
    screen = aScreen('Add A New Recipe','*','pass','Please provide instructions for prepairing your recipe or press "Q" to quit :> ',[nr.name,'Serves %s' % nr.servings,'Written by %s' % nr.source,' ','Ingredients'] + nr.ingredients,['pass'])
    nr.instructions = screen.showScreen()
    screen = aScreen('Add A New Recipe','*','pass','Do you wish to save this recipe? Press "Y" for YES and "N" for NO :> ',[nr.name,'Serves %s' % nr.servings,'Written by %s' % nr.source,' ','Ingredients'] + nr.ingredients + [' ','Preperation Instructions',nr.instructions],['pass'])
    kbd = screen.showScreen()
    # Now write all data to the database
    if kbd == 'Y':
        db.addRec(nr)

def DRD(db):
    screen = aScreen('Add A New Recipe','*','pass','Do you wish to continue? Press "Y" for YES and "N" for NO :> ',['WARNING!!! This section is for deleting recipes from your data base. This operation cannot be undone!'],['pass'])
    kbd = screen.showScreen()
    if kbd == 'Y':
        kbd = SAR(db,'delete')
        if kbd.isdigit() == True:
            db.deleteRec(kbd)

def mainLoop(dbname):
    db = newDB(dbname)
    t = 'The Cook Book Project > Main Menu'
    m = 'Press "Q" to quit or make a selection <: br="br">    o = ['Q','A','B','C','D','E']
    c = ['A - List All Recipes','B - Select A Recipe','C - Search Recipe Database',' ','D - "ADD" A New Recipe','E - "DELETE" A Recipe']
    mLscreen = aScreen(t,'*','pass',m,c,o)
    loop = True
    while loop == True: # Show the main menu until a valid option is selected.
        kbd = mLscreen.showScreen()
        if kbd == 'Q':
            loop = False
        elif kbd == 'A':
            # List all the recipes in the database.
            kbd = LAR(db,'The Cookbook Project > List All Records','Press any key to continue <: br="br">        elif kbd == 'B':
            # Select a recpie.
            SAR(db,'pass')
        elif kbd == 'C':
            # Search the data base.
            SRD(db)
        elif kbd == 'D':
            # Add a recipe.
            ARD(db)
        elif kbd == 'E':
            # Delete a recipe.
            DRD(db)
#------------------------------------------------------------------------------
#--|    Main Program    |------------------------------------------------------
# Title screen. Database file name will be asked for here.
intro = ['Welcome To The Cookbook Project','please enter the file name of your cookbook.']
title = aScreen('The Cookbook Project','*','pass','Press "Q" to quit or enter a file name <: br="br" intro="intro" pass="pass">dbname = title.showScreen()
# Main program loop.
if dbname != 'Q':
    if dbname == '':
        dbname ='cookbook1.db3' # This is a stub.
        mainLoop(dbname)
    else:
        mainLoop(dbname)

# Credits screen.
intro = ['The Cookbook Project Software.','Written by Kevin Lynch',' ','Original tutorial published by Full Circle Magazine.','Original code written by Greg Walters','Rainyday Solutions, LLC,','www.thedesignatedgeek.com',' ','Terminal dimentions detection by Grant Edwards']
title = aScreen(' ','*','centered','Press any key to quit <: br="br" intro="intro" pass="pass">dbname = title.showScreen()