Sunday, 3 August 2014

How I got started with Python Bottle on Windows8

Installation

If you are reading this you probably already know what Windows8, Python and Bottle are. The variety of Python I have is "Enthought Canopy Python 2.7.6 | 64-bit | (default, Jan 29 2014, 17:03:59) [MSC v.1500 64 bit (AMD64)] on win32". That is what appears when I launch Python in the Command Console. Enthought Canopy Python comes with a nice Python aware editor, and it's easy to manage the installation of packages, some of which Canopy would like you to pay for. But you don't need to. Just download the Windows installer for your version of Python and run it to install. I've gone back to my old ways of writing scripts in Notepad++.

Most of my Python scripts are small utility programs that I run from the Command Console or in their own WxPython window if there is a need for more interaction. The purpose of these is to extract text data from repositories of text files or XML files. Sometimes a lot of finding files on the basis of date which is encoded into the directory structure or file names, and a lot of parsing of text in a file once it has been found. Now I want to share these utilities with my colleagues so that can access this data as conveniently as I do. I could create stand-alone versions of these utilities with Py2exe or cx-freeze, but a far less painful way may be to access the utility with a browser, and have the Python done in the web server. That's why I'm looking at Bottle.

From the Bottle site, "Bottle is a fast, simple and lightweight WSGI micro web-framework for Python". Sounds like just what I need. There is a Tutorial, and that has download and installation instructions. It is obvious from the example commands that the intended audience for this is the Linux community. It seems doubtful that any of these would work in the Windows environment. I went out looking for a Windows installer for Bottle, or for anyone's experience with Bottle on Windows. Nothing.

Somewhere I saw that "pip" came standard with Python3x. I'm not on Python3 yet, but maybe Enthought had included "pip" - Enthought includes a lot of useful stuff which is why I use it. Now, Enthought Canopy installs Python kind of oddly on Windows, but maybe "pip" is on the Path which includes several paths that reference the Canopy Python installation. I could try a Windows version of the recommended install command! What could go wrong; right?


Wow! How easy was that? And the "Hello World!" script works just fine. So that is Bottle installed and working in a near trivial way. The rest of this article will be about building a practical application. I will assume the reader has read at least the Tutorial at the Bottle web page.

Loading a non-trivial page


I now have something substantial of my own that displays and functions. Woohoo! Here is the Python source for the Bottle web-framework:
from bottle import Bottle, run, static_file

app = Bottle()

@app.route('/') # Display a sort of index with active links
def default():
    return '''
<html>
<head>

<title>Index</title>
</head>
<body>
<h3>Contents:
</h3>
<a href="ExtractRegionals">ExtractRegionals</a> Extract Regional forecasts
</body>
</html>
'''

@app.route('/ExtractRegionals') # display page for Extract Regional forecasts
def ExtractRegionals():
    return static_file('ExtractRegionals.html', root='G:/RossMarsden/HTML/')

@app.route('<filepath:path>') # serve any static files that are required
def server_static(filepath):
    return static_file(filepath, root='G:/RossMarsden/HTML/')

run(app, host='localhost', port=8080, debug=True)


And this is the HTML page that loads when "http://localhost:8080/ExtractRegionals" is invoked in a browser:
<html>
<head>
<title>Extract Regionals (with DatePickr)</title>

<link href="DatePickr/datepickr.css" rel="stylesheet" type="text/css"></link>
<style type="text/css">
html {
 font:.8em Verdana, Arial, Helvetica, sans-serif;
 margin:5;
 padding:5;
    color:#000;
 background:#fff
}
.col { 
  border:1px solid green;
  margin: 1px;
  width: 220px;
  height: 210px;
  float:left;
}
.clear {
    clear:both;
}

</style>
<script src="DatePickr/datepickr.min.js" type="text/javascript"></script>
<script type="text/javascript">
regions = new Array('Northland', 'Auckland', 'Coromandel Peninsula', 'Waikato', 'Waitomo', 'Bay Of Plenty', 'Rotorua',
        'Taupo', 'Taumarunui', 'Taihape', 'Gisborne', 'Hawkes Bay', 'Wairarapa', 'Taranaki', 'Wanganui', 'Manawatu',
        'Horowhenua Kapiti Coast', 'Wellington', 'Marlborough', 'Nelson', 'Nelson_Lakes', 'Buller', 'Westland', 'Fiordland',
        'Canterbury Plains', 'Canterbury_High_Country', 'Christchurch', 'North Otago', 'Dunedin', 'Clutha',
        'Central Otago', 'Southern_Lakes', 'Southland');
function checkRegions(start, stop) {
    emit = '';
    for (var i = start; i < stop; i++) {
        emit += '<input id="' + regions[i] + '" type="checkbox">' + regions[i] + '<br>'
    }
    return emit;
}
function getRegionals() {
    var dateFrom = document.getElementById("dateFrom").value; 
    var dateTo = document.getElementById("dateTo").value;
    regionsList = new Array();
    for (var i = 0; i < regions.length; i++) {
        if (document.getElementById(regions[i]).checked) {
            regionsList.push(regions[i]);
        }
    }  
    document.getElementById("output").innerHTML = dateFrom + "[br]" + dateTo + "[br]" + regionsList + "[br]" + document.getElementById("extRa").checked; 
}
</script>
</head>
<body>
<h3>
Extract Regional Forecasts</h3>
From: <input id="dateFrom" size="16" />  
To: <input id="dateTo" size="16" />  
<input id="extRa" type="checkbox" /> include Extended Range   
<button onclick="getRegionals()">Retrieve Regionals</button>

<script type="text/javascript">
new datepickr('dateFrom', {'dateFormat': 'D d-M-Y'});
new datepickr('dateTo', {'dateFormat': 'D d-M-Y'});
</script>
<div id="selector">
<div class="col" id="col1">
<script type="text/javascript">
    document.write(checkRegions(0,parseInt(regions.length/3)))
</script></div>
<div class="col" id="col2">
<script type="text/javascript">
    document.write(checkRegions(parseInt(regions.length/3),parseInt(2*regions.length/3)))
</script></div>
<div class="col" id="col3">
<script type="text/javascript">
    document.write(checkRegions(parseInt(2*regions.length/3),regions.length))
</script></div>
<div class="clear">
</div>
</div>
<div height="500" id="output" width="660">
</div>
</body>
</html>

Note: In line 50, the square brackets should be changed to angle brackets in production. (The code renderer I have used here doesn't cope with tags in quotes and they leak into the page and execute!)

The features to notice with these two files are:
  1. Composition of a web page in the script, complete with working links; one in this case.
  2. Serving a static page from a specified location on the local system.
  3. Serving dependent static files that are located in a sub-directory of the location of the static page.
  Here is a screen shot of the page with one of the date pickers visible:
This just shows the page displaying and working in a simple and useless way. The next step is to convey the selection data (two dates, a list of locations and a logical variable) back to the server script so that the relevant forecasts can be retrieved from the repository and returned to be displayed in the browser.

Getting information back from the page

I plan to use the POST method to submit a form when the "Retrieve Regionals" button is pressed which means I need to construct these requester elements in the form of a form.

So, here is the program (script) that defines the server, and the requester HTML page follows below.

from bottle import Bottle, run, static_file, post, request

app = Bottle()

templatePage = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>%s</title>
</head>
<body>
%s
</body>
</html>
'''

@app.route('/') # Display a sort of index with active links
def default():
    title = 'Retrieve Forecasts'
    body = 'Options:<br><a href="RetrieveRegionals">Retrieve Regional forecasts</a>'
    return templatePage % (title, body)

@app.route('/RetrieveRegionals') # display page for Retrieve Regional forecasts
def RetrieveRegionals():
    return static_file('RetrieveRegionals.html', root='G:/RossMarsden/HTML/')

@app.route('<filepath:path>') # serve any static files that are required
def server_static(filepath):
    return static_file(filepath, root='G:/RossMarsden/HTML/')

@app.post('/DisplayRequestedRegionalFc')
def DisplayRequestedRegionalFc():
    # obtain data from the request form
    dateFrom = request.forms.get('dateFrom')
    dateTo = request.forms.get('dateTo')
    regionsList = request.forms.get('regionsList')
    extRa = request.forms.get('extRa') # either "on" or None
    title = 'Regional Forecasts'
    # More substantial code would go here to obtain the requested forecasts from the repository, 
    # but for the purpose of demonstration this will suffice.
    retrievdForecasts = '%s\n%s\n%s\n%s\n' % (dateFrom, dateTo, regionsList, extRa)
    # build the body for the output page
    emit = []
    emit.append('<h3>%s</h3>' % title)
    emit.append('Regional forecasts issued between %s and %s[br]' % (dateFrom, dateTo))
    emit.append('for %s %sincluding Extended Range forecasts[br]' % (regionsList, ['','not '][extRa == None]))
    emit.append('<textarea cols=80 rows=40>%s</textarea>[br]' % retrievdForecasts)
    emit.append('Back to <a href="RetrieveRegionals">Retrieve Regional forecasts</a> page[br]')
    emit.append('Back to <a href="/">Retrieve forecasts index</a> page[br]')
    return templatePage % (title, '\n'.join(emit))
    
run(app, host='localhost', port=8080, debug=True)


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Retrieve Regionals (with DatePickr)</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" href="DatePickr/datepickr.css" />
<style type="text/css">
html {
 font:.8em Verdana, Arial, Helvetica, sans-serif;
 margin:5;
 padding:5;
    color:#000;
 background:#fff
}
.col { 
  border:1px solid green;
  margin: 1px;
  width: 220px;
  height: 210px;
  float:left;
}
.clear {
    clear:both;
}
#regionsList {
    display: none;
}

</style>
<script type="text/javascript" src="DatePickr/datepickr.min.js"></script>
<script type="text/javascript">
regions = new Array('Northland', 'Auckland', 'Coromandel Peninsula', 'Waikato', 'Waitomo', 
        'Bay Of Plenty', 'Rotorua', 'Taupo', 'Taumarunui', 'Taihape', 'Gisborne', 'Hawkes Bay',
        'Wairarapa', 'Taranaki', 'Wanganui', 'Manawatu', 'Horowhenua Kapiti Coast', 'Wellington', 
        'Marlborough', 'Nelson', 'Nelson_Lakes', 'Buller', 'Westland', 'Fiordland', 
        'Canterbury Plains', 'Canterbury_High_Country', 'Christchurch', 'North Otago',
        'Dunedin', 'Clutha', 'Central Otago','Southern_Lakes','Southland');
        
function checkRegions(start, stop) {
    emit = '';
    for (var i = start; i < stop; i++) {
        emit += '<input id="' + regions[i] + '" type="checkbox" onchange="updateRegionsList()">' + regions[i] + '[br]'
    }
    return emit;
}
function updateRegionsList() {
    regionsList = new Array();
    for (var i = 0; i < regions.length; i++) {
        if (document.getElementById(regions[i]).checked) {
            regionsList.push(regions[i]);
        }
    }  
    document.getElementById("regionsList").value = regionsList; 
}
</script>
</head>
<body>
<h3>Retrieve Regional Forecasts</h3>
<form action="/DisplayRequestedRegionalFc" method="post">
From: <input id="dateFrom" name="dateFrom" size="16" />&nbsp;&nbsp;
To: <input id="dateTo" name="dateTo" size="16" />&nbsp;&nbsp;
<input id="extRa" name="extRa" type="checkbox" > include Extended Range&nbsp;&nbsp; 
<input value="Retrieve Regionals" type="submit" /><br>
<input id="regionsList" name="regionsList" size="60" readonly type="text" />
</form>
<script type="text/javascript">
new datepickr('dateFrom', {'dateFormat': 'D d-M-Y'});
new datepickr('dateTo', {'dateFormat': 'D d-M-Y'});
</script>
<div id="selector">
<div id="col1" class="col">
<script type="text/javascript">
    document.write(checkRegions(0,parseInt(regions.length/3)))
</script></div>
<div id="col2" class="col">
<script type="text/javascript">
    document.write(checkRegions(parseInt(regions.length/3),parseInt(2*regions.length/3)))
</script></div>
<div id="col3" class="col">
<script type="text/javascript">
    document.write(checkRegions(parseInt(2*regions.length/3),regions.length))
</script></div>
<div class="clear"></div>
</div>
<div id="output" width=660 height=500>
</div>
</body>
</html>

One think that puzzled me for a short while is that in HTML and Javascript, for elements in forms, "id" is used to refer to its properties, styles, value, etc, but its "name" is used to access values for the purpose of the POST method. So you need both an "id" and a "name" for form elements.

This is all I want to post about this, for now.