SQLite/Bottle Todo List

I wanted to do a Todo List web application that I could pass on to my kids to try. My goal was to give them an introduction to SQL, Web interfaces and Web templating.

For the Todo List application the Python Bottle Web Framework will be used. The Bottle library is a lightweight standalone micro web framework.

To store the Todo list items an SQLite database is used. SQLite is a file based server-less SQL (Structured Query Language) database which is ideal for small standalone applications.

Finally to build Web page, Python web templates will be used. The advantage of using web templates is that is reduces the amount of code written and it separates the presentation component from the backend logic.

 

Getting Started with Bottle

To install the Python bottle library:

pip install bottle

As a test we can make a program that has a home page (“/”) and a second page, then links can be put on each of the page to move back and forward.

todo_code_1

The @route is a decorator that links a URL call, like “/” the home page to a function. In this example a call to the home page “/” will call the home_page() function.

In the home_page() and pages2() functions the return call is used to pass HTML text to the web browser. The anchor tag (<a) is used to define page links.

The run() function will start the Bottle micro web server on: http://127.0.0.1:8080/

The output is below. 

SQLite

The Python SQLite library is one of the base libraries that is installed with Python.

SQLite has a number of tools and utilities that help manage your databases. One useful  light weight application is: DB Browser for SQLite. It is important to note that SQLite data can be view by multiple applications, but for edits/deletes only 1 application can be accessing SQLite.

For the Todo list we’ll start with a simple database structure using three fields:

  • Category – this a grouping such as: shopping, projects, activities etc.
  • theItem – this is the actual to do item
  • id – an unique index for each item. (This will used later for deleting rows)

The Todo database can be created with the SQLite Brower, or in Python code. The code below creates that database, adds a todo table and then inserts some records.


import sqlite3

print("Create a todo list database...")

conn = sqlite3.connect('todo.db') # Warning: This file is created in the current directory
conn.execute("CREATE TABLE todo (category char(50), theitem char(100),id INTEGER PRIMARY KEY )")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Shopping','eggs')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Shopping','milk')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Shopping','bread')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Activities','snow tires')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Activities','rack lawn')")
conn.commit()

print("Database todo.db created")

The DB Browser can be used to view the newly created database.

make_table

Viewing the Data in Python

An SQL SELECT command is used to get all the records in the todo database.  A fetchall() method is will return all the database rows in a Python tuple variable. Below is the code to write the raw returned data to a browser page.

# Send to raw SQL result to a Web Page
#
import sqlite3
from bottle import route, run

@route('/')
def todo_list():
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()
    
    c.execute("SELECT * FROM todo")
    result = c.fetchall()
    c.close()
# note: the SQL results are an array of data (tuple)
# send results as a string
    return str(result)

run()

todo_raw

The output formatting can be improved by adding a sort to the SQL SELECT statement and then HTML code are be used to show category heading.

todo_code_2

web_rbr

For small applications putting HTML code in the Python code is fine, however for larger web projects it is recommended that the HTML be separated out from the database or backend code.

Web Templates

Web templates allow you to separate the database and back end logic from the web presentation. Bottle has a built-in template engine called Simple Template Engine. I found it did everything that I needed to do. It’s possible to use other Python template libraries, such as Jinja, Mako or Cheetah, if you feel you need some added functionality.

The earlier Python code is simplified by removing the HTML formatting logic. A template object is created with a template name of  sometime.tpl). An example would be:

output = template(make_table0, rows=result, headings = sqlheadings)

Where rows and headings are variable names that are used in the template file. The template file make_table0.tpl is in the working directory.

#
# Build a To Do List with a Web Template
#
import sqlite3
from bottle import route, run, template


@route('/')
def todo_list():
    # Send the output from a select query to a template
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()    
    c.execute("SELECT * FROM todo order by category, theitem")
    result = c.fetchall()
    # define a template to be used, and pass the SQL results
    output = template('make_table0', rows=result)
    return output
        
run()

Templates are HTML pages with inline Python code. Python code can either be in blocks with a <% to start the block and a %> to end the block, or each line can start with %.

Two of the major differences of inline template Python code are:

  • indenting the line of Python is not required or used
  • control statements like : if and for need an end statement

A template that takes the SQL results and writes each row in a table would look like:

tpl_make_table

todo_tmp_table

A template that take the SQL results and writes a category heading and lists the items would look like:

tpl_make_list

web_tpl_rbr

Include ADD and DELETE to the Templates

The next step is to include ADD and DELETE functionality.

For the DELETE functionality, a form is added to the existing template. A ‘Delete’ button is placed beside all the Todo items and the button passes the unique index id (row[2]) of the item. The form has a POST request  with the action going to the page ‘/delete‘ on the Bottle Web Server.

For the ADD functionality, a new template is created and a %include call is put at the bottom of the main template. (You could also put everything in one file).

The main template now looks like:

tpl_show_todo

The new item template uses a dropdown HTML element with some predefined category options (Activities, Projects and Shopping). The item text will displace 25 characters but more can be entered. Pressing the save button will generate a POST request to the “/new” URL on the Bottle server.

The new_todo.tpl file is:

tpl_newtask

Bottle Python Code with /add and /delete Routes

The final step is to include routes for the /add and /delete URL references.

The new_item() function gets the category and Todo item from the form in the new_todo template. If the request passes a non-blank item (request.forms.get(“theitem”) then an SQL INSERT command will add a row. The unique item ID is automatically included because this field is defined as an index key.

The delete_item() function reads the unique item id that is passed from the button to issue an SQL DELETE statement.

At the end of the new_item() and delete_item() function the user is redirected back to the home (“/”) page.

#
# Build a Todo List 
#
import sqlite3
from bottle import route, run, template, request, redirect, post  

# The main page shows the Todo list, /new and /delete references are called from this page
@route('/')
def todo_list():
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()
    
    c.execute("SELECT * FROM todo order by category,theitem ")
    result = c.fetchall()
    # in case column names are required
    colnames = [description[0] for description in c.description]
    numcol = len(colnames)
    # for now only the rows=result variables are used
    output = template('show_todo', rows=result, headings=colnames, numcol = numcol)
    return output

# Add new items into the database
@route('/new', method='POST')
def new_item():

    print("New Post:", request.body.read())
    theitem = request.forms.get("theitem")
    newcategory = request.forms.get("newcategory")

    if theitem != "":        
        
        conn = sqlite3.connect('todo.db')
        c = conn.cursor()
        c.execute("INSERT INTO todo (category,theitem) VALUES (?,?)", (newcategory,theitem))
        conn.commit()
        c.close()

    redirect("/") # go back to the main page   

# Delete an item in the database
@route('/delete', method='POST')
def delete_item():

    print("Delete:", request.body.read() )
    theid = request.forms.get("delitem").strip()
    print("theid: ", theid)
               
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()
    sqlstr = "DELETE FROM todo WHERE id=" + str(theid)
    print(sqlstr)
    c.execute(sqlstr)
    conn.commit()
    c.close()

    redirect("/") # go back to the main page   
        
run()

The application will look something like:

todo_final

Final Clean-up

Some of the final clean-up could include:

  • enlarge the database to include fields like: status, due date, who is responsible etc.
  • add an “Are you sure?” prompt before doing adds and deletes
  • verify double entries aren’t included
  • include an edit feature
  • make the interface slicker

If you want to speed up the performance PyPy can be used instead of the Python interpreter. To use Pypy (after you’ve installed it), you will need to install the pip and bottle:

pypy3 -m ensurepip --user
pypy3 -mpip install bottle --user

Final Comments

As I was working on this I found a good BottleTutorial: Todo-List Application. This tutorial approaches the Todo list project differently but it is still a worthwhile reference.

Pi Rover using a Bottle Web Framework

There are a lot of Python web library choices, with each of the libraries offering different features and functions.

For simple Raspberry Pi Web application I was really impressed by the Python Bottle library. In this blog I’d like to document a Raspberry Pi rover project that I did using bottle.

Getting Started

To install the Python bottle library:

pip install bottle

There are some good tutorials for bottle. Probably the most important item is defining a decorator for a URL, and then linking the decorator to a function:

@route('/') # define a decorator for the start page
def my_homefuntion() # call a function for the start page
   return static_file("startpage.htm", root='') # call the start page

@route('/otherpage') # define a decorator for another page 
def my_otherpage_function() # call a function for the start page
   # do some stuff...

For the RaspPi rover I used a low cost car chassis (~$15).

It is not recommended to connect motors directly to Rasp Pi pin, for a few reasons:

  • larger motors require more power that a Pi GPIO pin can supply
  • power surges on motors can damage the Pi hardware
  • forward/backward motor directions require extra hardware
  • GPIO pins will only do ON/OFF, no variable speed settings.

There are a number of Raspberry Pi motor shield that can be used. For this project I used the Pimoroni Explorerhat Pro ($23). The Explorerhat has an excellent Python library that allows you to easily control the motor’s direction and speed.

Web Page

The goal for the web page (bottle1.htm) was to have 5 buttons for the controls and use AJAX to send the action request and then return the action code and display it on the page. The returned action would appear above the buttons ( in the lastcmd  paragraph tag).

Screen_bottle

For this example the button push was sent in the header as a GET request. So a forward button push would be a myaction: forward header item. In previous projects I’ve used POST request with parameters, however I found that header items can make things a little simpler.

<!DOCTYPE html>
<html>
<head> 
<title>Python Bottle Rover</title> 
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,"> 
<style>
  html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;}
  h1{color: #0F3376; padding: 2vh;}p{font-size: 1.5rem;}
  .button{display: inline-block; background-color: #4286f4; 
  border-radius: 4px; color: white; font-size: 30px; width:100%; height: 75px}
  .button2{background-color: green ;width:31%}
  .stop{background-color: red; width:33%}
</style>
</head>
<script>
function sendcmd(thecmd) {
  // send the action as a header item 
  var xhttp = new XMLHttpRequest();
  xhttp.open("GET","/action" , true);
  xhttp.setRequestHeader("myaction", thecmd);
  xhttp.send()
  xhttp.onreadystatechange = function() {
	// Get the response and put on it the screen
	if (this.readyState == 4 ) {	
		document.getElementById("lastcmd").innerHTML = "Last Command:<b>" +xhttp.responseText;
	}
  }
}
</script>
 
<body>
<h2>Python Bottle Rover</h2> 
<p id='lastcmd'></p>
<button onclick="sendcmd('forward')" class="button">FORWARD</button>
<button onclick="sendcmd('left')" class="button button2">LEFT</button>
<button onclick="sendcmd('stop')" class="button stop">STOP</button>
<button onclick="sendcmd('right')" class="button button2" >RIGHT</button>
<button onclick="sendcmd('backward')" class="button button">BACKWARD</button>
  
</body>
</html>

 

Bottle Rover App

For the rover app, there are two URL endpoints. The root (/) which would display the bottle1.htm page, and an action (/action) URL which would only be called from AJAX script when a button was pushed.

For this project the Raspberry Pi ip address was hardcoded into the code, for future projects dynamically getting the ip would be recommend. Also a web port of 8000 was used so as to not conflict with a dedicated web server (like Apache) that could be running on the Pi.

# Bottle2rover.py - web control for a RaspPi rover<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>
#
from bottle import route, run, static_file, request

# Define RaspPi I/O pins
import explorerhat

# Send Action to Control Rover
def rover(action):
  if action == "forward":
    explorerhat.motor.one.speed(100)
    explorerhat.motor.two.speed(100)
  if action == "left":
    explorerhat.motor.one.speed(100)
    explorerhat.motor.two.speed(0)
  if action == "right":
    explorerhat.motor.one.speed(0)
    explorerhat.motor.two.speed(100)
  if action == "stop":
    explorerhat.motor.one.speed(0)
    explorerhat.motor.two.speed(0)
  if action == "backward":
    explorerhat.motor.one.speed(-50)
    explorerhat.motor.two.speed(-50)

# use decorators to link the function to a url
@route('/')
def server_static():
  return static_file("bottle1.htm", root='')
# Process an AJAX GET request, pass the action to the rocket launcher code
@route('/action')
def get_action():
  action = request.headers.get('myaction')
  print("Requested action: " + action)
  rover(action)
  return request.headers.get('myaction')

# Adjust to your required IP and port number
run(host = '192.168.0.106', port=8000, debug=False)

Final Comments

I was happily surprised how easy it was to get a Python bottle web app running. The bottle documentation was straightforward and I found that the code was quite lean.

Pan-Tilt-Shoot Webcam

PTZ or Pan-tilt-zoom cameras are off the shelf and reasonably low cost. This project is a home made pan-tilt camera that shoots nerf rockets.

I got the rocket launcher in a bargain bin, but they can be found online starting around $15.

For this project there were 3 main steps:

  • getting the Python code talking to the rocket launcher
  • loading some USB webcam software
  • loading a Python web framework (bottle).

The hardware could be a PC (preferably running Linux) or a Raspberry Pi. On this project I used a Pi clone (an Orange Pi Lite). The faster your hardware the better the streaming video performance that you’ll get.

pts_overview

Getting the Rocket Launcher Working

There are a number of Python libraries that need to loaded:

pip install setuptools
pip install usb

To control the rocket launcher Python can connect to the USB device using the vendor id (0x2123) and product id (0x1010). Rocket launcher commands are issued as USB transfer codes.

A command line test program (rocket1.py) would be:

 import usb  
 import sys  
 import time  
 device = usb.core.find(idVendor=0x2123, idProduct=0x1010)  
 # On Linux we need to detach usb HID first  
 try:  
   device.detach_kernel_driver(0)  
 # except Exception, e:  
 except Exception:  
   pass # already unregistered  
 device.set_configuration()  
 endpoint = device[0][(0,0)][0]  
 down = 1 # down  
 up = 2 # up  
 left = 4 # rotate left  
 right = 8 # rotate right  
 fire = 16 # fire  
 stop = 32 # stop  
 #device.ctrl_transfer(0x21, 0x09, 0x0200, 0, [signal])  
 while True:  
   print('r = right, l = left, u = up, d = down, f = fire ')  
   key = raw_input ('enter key:')  
   if (key == 'l'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, left, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (key == 'u'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, up, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (key == 'r'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, right, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (key == 'd'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, down, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (key == 'f'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, fire, 0x00,0x00,0x00,0x00,0x00,0x00])  
     time.sleep(4)  
   time.sleep(0.1)  
   device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, stop, 0x00,0x00,0x00,0x00,0x00,0x00])  

By default the USB port requires superuser rights, so run the program using sudo:

$ sudo python rocket1.py
r = right, l = left, u = up, d = down, f = fire
enter key:r
r = right, l = left, u = up, d = down, f = fire
enter key:

Getting the Web Cam working

A standard USB Web Cam can be connected to a Linux  PC or Raspberry Pi using the  motion package. Motion is super easy to setup and it’s got lots of added features if you want to look at enhancing things later. To install motion:

sudo apt-get install motion

Once you have motion installed you’ll need to tweek some of it’s parameters by:

sudo nano /etc/motion/motion.conf

The /etc/motion/motion.conf file contains a lot of  parameters, some of the more important ones are:

# Image width (pixels). Valid range: Camera dependent, default: 352
width 800

# Image height (pixels). Valid range: Camera dependent, default: 288
height 600

# Maximum number of frames to be captured per second.
framerate 1

# Maximum framerate for stream streams (default: 1)
stream_maxrate 1

# Restrict stream connections to localhost only (default: on)
stream_localhost off

The speed of your hardware and network will determine how many frames per second you can use.

To run the video server enter:

sudo motion &

The motion package has a built in web server that is accessed by: http://your_ip:8081

livevideo

Getting the Bottle Web Server Running

There are a lot of web programming options that are available. In my case I wanted to run the project as a simple standalone app on an Orange Pi (a Raspberry Pi clone). To install bottle:

sudo apt-get install python-bottle

The Pan-Tilt-Shoot Web page (ptscam.html) was designed with 5 buttons and the Web Cam video below. When a button is clicked a javascript function called sendcmd passes a command as a header item in a AJAX request.  The motion web server camweb video is included using the html img tag, with the source being the web link.

<!DOCTYPE html>
<html>
<head> 
<title>Pan-Tilt-Shoot Webcam</title> 
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,"> 
<style>
  html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;}
  h1{color: #0F3376; padding: 2vh;}p{font-size: 1.5rem;}
  .button{display: inline-block; background-color: #4286f4; border: none; 
  border-radius: 4px; color: white; text-decoration: none; font-size: 30px; width:100%}
  .button2{background-color: green ;width:31%}
  .button3{background-color: red; width:33%}
</style>
</head>
<script>
function sendcmd(thecmd) {
  // send the action as a header item 
  var xhttp = new XMLHttpRequest();
  xhttp.open("GET","/action" , true);
  xhttp.setRequestHeader("myaction", thecmd);
  xhttp.send()
}
</script> 
<body>
<h2>Pan-Tilt-Shoot Webcam</h2> 
<button onclick="sendcmd('up')" class="button">UP</button>
<button onclick="sendcmd('left')" class="button button2">LEFT</button>
<button onclick="sendcmd('fire')" class="button button3">FIRE</button>
<button onclick="sendcmd('right')" class="button button2" >RIGHT</button>
<button onclick="sendcmd('down')" class="button button">DOWN</button>
  
<p><img src='http://192.168.0.117:8081/'></p>
</body>
</html>

The Python web server application sends the web page at startup, and then it processes AJAX requests and passed the requested action (as a header item) to the rocket launcher USB device.

 # Python Bottle   
 #  
 import os, socket  
 from bottle import route, run, static_file, request  
 import usb  
 import sys  
 import time  
    
   
 # Send an action to the rocket launcher  
 def do_action(theaction):  
    
   print("Action : " + theaction)  
   down = 1 # down  
   up = 2 # up  
   left = 4 # rotate left  
   right = 8 # rotate right  
   fire = 16 # fire  
   stop = 32 # stop  
   if (theaction == 'left'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, left, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (theaction == 'up'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, up, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (theaction == 'right'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, right, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (theaction == 'down'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, down, 0x00,0x00,0x00,0x00,0x00,0x00])  
   if (theaction == 'fire'):  
     device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, fire, 0x00,0x00,0x00,0x00,0x00,0x00])  
     time.sleep(4)  
   time.sleep(0.1)  
   device.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, stop, 0x00,0x00,0x00,0x00,0x00,0x00])  

# Send the starting page   
 @route('/')  
 def server_static():  
   return static_file("ptscam.html", root='')  
# Process an AJAX GET request, pass the action to the rocket launcher code   
 @route('/action')  
 def get_action():  
   print("Requested action: " + request.headers.get('myaction'))  
   do_action(request.headers.get('myaction'))  
       
   
 # On Linux we need to detach usb HID first  
 device = usb.core.find(idVendor=0x2123, idProduct=0x1010)  
 try:  
   device.detach_kernel_driver(0)  
 except Exception:  
   pass # already unregistered  
   
 # Start the bottle web server  
 run(host="192.168.0.117" , port=8000, debug=False)  

Because of the USB connection the Python application need to be run under sudo. When the program is running some diagnostics will show connections and actions.

$ sudo python ptscam.py
Bottle v0.12.13 server starting up (using WSGIRefServer())...
Listening on http://192.168.0.117:8000/
Hit Ctrl-C to quit.

192.168.0.114 - - [23/Nov/2019 19:26:24] "GET / HTTP/1.1" 200 1251
192.168.0.114 - - [23/Nov/2019 19:26:24] "GET /:8081 HTTP/1.1" 404 736
192.168.0.114 - - [23/Nov/2019 19:26:53] "GET / HTTP/1.1" 200 1271
Requested action: up
Action : up

The web page is accessed by : http://your_ip:8000

pts_screen

Final Comments

The next step will be to mount the rocket launcher with the USB web cam on a little rover.