Troubleshooters.Com®, Code Corner and Python Patrol Present:
The Bottle Lightweight Rapid Development Tool
Copyright © 2017 by Steve Litt
See the Troubleshooters.Com Bookstore.
CONTENTS
Bottle is a Python addon to easily and quickly construct a small program whose user interface is revealed on a web browser. It's a much quicker construction than old Perl/CGI apps, and unlike behemoths like Rails and Django, requires little scaffolding. It's a breeze to deploy:
Bang, you're deployed!
Let's build a minimal Bottle app. Create directory $HOME/bottlehello to contain this application. Now download the current stable version of bottle.py from https://bottlepy.org/docs/dev/ into $HOME/bottlehello. Create the following hellobottle.py in your $HOME/bottlehello directory:
import bottle @bottle.route('/hello', method='GET') def doit(): return '<p>Hello World from Bottle</p>' bottle.run(port=43234, debug=True)
In the preceding, the first line imports bottle. The next line "routes" bottle; that is to say any GET requests that come over localhost port 43234 for a URL suffix of /hello cause
Next, let's run the app. Within that same $HOME/bottlehello directory, run the following command:
python3 hellobottle.py
By importing bottle.py, your hellobottle.py program not only acts as a web app, but as a server also. You don't need to endlessly mess with Apache to get it to run on port 43234.
Now view the 127.0.0.1:43234/hello. The browser should return saying "Hello World from Bottle". If it doesn't, troubleshoot until it does.
Warning!
Make sure the exact URL is 127.0.0.1:43234/hello
Make sure there is no slash at the end of the word "hello", and make sure there are not two slashes before that word. Web apps like Bottle derived apps are much more sensitive to exact URLs.
The "Hello World" app in the preceding section is viable only on the computer hosting the program. To get it to run on the LAN Intranet, just declare its host as the computer's actual IP address. So if the computer's IP address is 192.168.100.2, then change the bottle.run() code to look like the following:
bottle.run(host="192.168.100.2", port=43234, debug=True)
The only remaining challenge is to get your firewall to let port 43234 through. If you have iptables, the way I accomplished this is by adding the following two lines to my /etc/iptables/iptables.rules:
-A INPUT -p tcp --dport 43234 -j ACCEPT -A OUTPUT -p tcp --dport 43234 -j ACCEPT
I then restarted IPTables, and everything worked. When I browsed to 192.168.100.2:43234/hello from a computer at 192.168.100.236, it worked perfectly.
First, be extremely aware that leaving your application accessible on the Internet requires a whole lot more security than on your LAN, whose access you probably control physically. Cleanse all your inputs. Make sure no input ends up on a command line. Run no executables from your web app. Be careful.
Once you've done all of that, simply prick a pinhole in your Internet facing Router/firewall. Let 43234 come in and out, and set it so destination port 43234 on your router's Internet address NATs (Network Address Translation) to that same port on 192.168.100.2, which you set up to work on the LAN in the Hello on the LAN Intranet section. Test from another place on the Internet.
Warning!
When testing your setup from another place on the Internet, it really needs to be another place: A place outside of your LAN's router/firewall where the domain name resolves correctly. I suggest ssh'ing to another server outside your LAN, and then using lynx, links, or elinks to access your "hello world" web app.
In Bottle, the word "routing" refers to the fact that different URLs cause the web app to do different things. Different URLs "route" to different functions to run. Consider the following program:
import bottle @bottle.route('/hello', method='GET') def doit(): return '<p>Hello World from Bottle</p>' @bottle.route('/goodbye', method='GET') def doit(): return '<p>Goodbye Cruel World</p>' @bottle.route('/hello/special', method='GET') def doit(): return '<p>Special Hello World from Bottle</p>' @bottle.route('/hello/<num:int>', method='GET') def doit(num): return bottle.template('<p>Hello customer {{n}}</p>', n=num) bottle.run(host='127.0.0.1', port=43234, debug=True)
So, by defining these "routes", you send one message when they go to localhost:43234/hello, a different one for /hello/special, another for /goodbye, and a special message for /hello/ followed by a number. Note that /hello/1234 works, but /hello/12z34 comes back with "not found".
This is incredibly powerful, because there are an infinite number of URLs that can be used, each repesenting a separate functionality. The final route deserves special consideration:
@bottle.route('/hello/<num:int>', method='GET') def doit(num): return bottle.template('<p>Hello customer {{n}}</p>', n=num)
Notice the angle brackets surrounding the data after the final slash. This identifies this final stage as a variable to be used by the associated function, not a string. This variable MUST not contain any slashes. Notice that the variable name is num and it's of type int. It didn't have to be any particular type, but if that variable should always be a number, this is one additional way to sanitize inputs.
Notice that function doit() delivers num as an argument, so doit() can work with the variable.
This leaves the call to template(). Bottle templates are a huge work saver, discussed in another section. Let's just pick apart this particular usage:
return bottle.template('<p>Hello customer {{n}}</p>', n=num)
Tempates always return a string. Templates consist (mostly) of HTML, with some variables intermixed. Variables are delineated by double open squiggly and double closing squiggly braces: In this case, {{n}}. But where does that argument come from?
It comes from the non-string argument to template(), n=num. What that argument means is that the variable is known on the inside of the template as n, but on the outside of the template as num.
If you were in the business around the turn of the century, you remember the bad old days of CGI scripts, where you had to construct every single piece of HTML with (usually Perl) code. It was tedious and error prone. Bottle templates are meant to free you from all that HTML construction: The string contains HTML and variables, and you take care of the variabls outside of the template. There's an example later in this document.
Templates can also include special Python code to do further work, but in my opinion, I'd rather try to get it all done just with variables. A clever use of a variable can often substitute for an if statement.
Somebody's already done a great CRUD first attempt, so I'm not going to copy it. Browse to https://www.toptal.com/bottle/building-a-rest-api-with-bottle-framework and start reading the page at the "Building Your REST API" heading. Go through his routing discussion.
His implementation is indeed a great http based CRUD REST API. But it isn't a browser based web app: browsers operate on HTML, not HTTP, and although HTTP specifies many comunication verbs such as GET, POST, PUT, DEL, and others, HTTP 4 and below only specify, and only transmit, GET and POST. Also, by the time you include forms for the user to fill in on create and edit, and to look at for read and delete, the application becomes quite a bit more complex.
CRUD stands for Create, Read, Update, Delete, which are the four things you do to data. A CRUD app gives the user an easy facility to do these four functionalities, although in many cases, Read is just Update when the user chooses not to alter anything. A CRUD app starts with a list of rows, with each row having an update button and a delete button. There's also one button on the form (usually at the top) to add a brand new record (Create).
Every user action consists of these steps:
If the app is written efficiently, each of the three or four necessary forms are created from the same HTML template, with things like button names and field enablement changed by simple variables. Here's the form template I used:
<form action="{{c['url']}}" method="{{c['verb']}}"> <p>{{c['fcnname']}} People Row for Unique ID {{r['uid']}}</p> <p></p> <label for="fname">First name: </label>< input type="text" name="fname" value="{{r['fname']}}" size="15" maxlength="40" id="fname" {{c['viewonly_phrase']}}/><br/> <label for="lname">Last name: </label>< input type="text" name="lname" value="{{r['lname']}}" size="15" maxlength="40" id="lname" {{c['viewonly_phrase']}} /><br/> <label for="job">Job: </label>< input type="text" name="job" value="{{r['job']}}" size="15" maxlength="40" id="job" {{c['viewonly_phrase']}} /><br/> <label for="uid">Unique ID:</label>< input type="text" name="uid_scratch" value="{{r['uid']}}" size="12" disabled="disabled" id="uid" bgcolor="#ff0000" /><br/> <br/><button name="accept" type="submit">Accept</button> <button name="cancel" type="submit">Cancel</button> <input type="number" name="uid" value="{{r['uid']}}" size="12" readonly="readonly" hidden="hidden"/> </form>
If you look at the preceding template for a few minutes with some contemplation, you 'll see that almost everything about the form can be changed just by changing dictionaries r, which is row information, and c, which is context information like the function to be performed and whether fields should be enabled.
I put together a simple CRUD application with lots of deficiencies, but it does illustrate the points. Never use this CRUD app anywhere unknown people might be using it, especially on the Internet.
The following is a screenshot of this app's main screen: A list of records:
And here are the Create, Update and Delete forms, in that order:
And here is the app's source code, keeping in mind that its form template file was defined in the preceding section:
#!/usr/bin/python3 import sys import bottle from bottle import request, response, template from bottle import post, get, put, delete # GLOBAL CONSTANTS PORT=43234 HOST='127.0.0.1' # GLOBAL PEOPLE DICTIONARY people={} people['1001'] = {'lname':'Torvalds', 'fname':'Linus', 'job':'Manager'} people['1002'] = {'lname':'Stallman', 'fname':'Richard', 'job':'Chief Freedom Officer'} people['1003'] = {'lname':'Foreman', 'fname':'George', 'job':'Boxer'} # UID HANDLER CLASS AND GLOBAL VAR class Uid_handler: def __init__(self): self.nextuid = 1010 def get(self): return self.nextuid def inc(self): self.nextuid += 1 def getinc(self): tmp = self.get() self.inc() return tmp uidObj = Uid_handler() # DATABASE MODIFIER ROUTINES def delete_people_row(uid): del people[str(uid)] def update_people_row(uid, dic): for key in dic.keys(): try: people[str(uid)][key] = dic[key] except: msg='Failed to update row {}, key {} with value {}' print(msg.format(uid, key, people[key])) def create_people_row(uid, dic): people[str(uid)] = dic # DATABASE READER ROUTINES def list_people(people): listt = []; for keyy in sorted(people.keys()): print(keyy) tmp=people[keyy]; tmp['uid'] = keyy listt.append(tmp) return listt def get_person(uid): print('dia in get_person() uid = {}.'.format(uid)) print('Not currently used') return people[str(uid)] # LOW LEVEL HTML ASSEMBLERS def link_primative(): return """ <a style="text-decoration: none; font-size:150%; font-weight:bold; font-style: italic; color: #>f; """ def assemble_icons(uid): sDel= link_primative() sEdit=sDel + 'background-color: #000099;"' + ' href="{}"' + '>E</afffff' sDel=sDel + 'background-color: #990000;"' + ' href="{}"' + '>D</afffff' sEditURL = 'http://{}:{}/people/forms/update/{}' sEditURL=sEditURL.format(HOST, PORT, uid) sDelURL = 'http://{}:{}/people/forms/delete/{}' sDelURL=sDelURL.format(HOST, PORT, uid) sEdit=sEdit.format(sEditURL) sDel=sDel.format(sDelURL) iconAssy=sDel + ' ' + sEdit return iconAssy def assemble_line(uid,lname,fname,job): line='<p>{} ::: {}, {}, {}, ({})eeeee/pfffff' iconAssy=assemble_icons(uid) line=line.format(iconAssy, lname, fname, job, uid) return line # FUNCTION TO PRINT PERSON LIST TO BROWSER @get('/people') def return_list(): rtrn=""" <h1>Here are your recordseeeee/h1fffff <a href="http://{}:{}/people/forms/create">eeeeebuttonfffffNew Record</button>eeeee/afffff """ rtrn = rtrn.format(HOST, PORT) lst = list_people(people) for row in lst: st = assemble_line(row['uid'], row['lname'], row['fname'], row['job']) rtrn += st return(rtrn) # FUNCTIONS CALLED BY FORMS, THAT CALL DATABASE MODIFIERS # AND ALSO RERUN THE PEOPLE LIST UPON COMPLETION @post('/people/delete/<name:int>') def delete_handler(name): if request.forms.get('accept') == None: return return_list() delete_people_row(name) return return_list() @post('/people/update/<name:int>') def update_handler(name): if request.forms.get('accept') == None: return return_list() fields = {} fields['fname'] = request.forms.get('fname') fields['lname'] = request.forms.get('lname') fields['job'] = request.forms.get('job') print('dia1') update_people_row(name, fields) print('dia2') return return_list() # FUNCTIONS THAT RETURN A FORM @get('/people/forms/delete/<name:int>') def return_del_form(name): context = {} # context info row = {} # row info context['url'] = '/people/delete/{}'.format(str(name)) context['verb'] = 'post' # Actual http verb "delete" not in html4 context['fcnname'] = 'Delete' context['viewonly_phrase'] = ' disabled="disabled" ' row = get_person(name) return template('people/crudform', r=row, c=context) @get('/people/forms/update/<name:int>') def return_update_form(name): print('dia top return_update_form()') context = {} # context info row = {} # row info context['url'] = '/people/update/{}'.format(str(name)) context['verb'] = 'post' # Actual http verb "delete" not in html4 context['fcnname'] = 'Update' context['viewonly_phrase'] = '' row = get_person(name) return template('people/crudform', r=row, c=context) @get('/people/forms/create') def return_create_form(): uid = uidObj.getinc() fields = {} fields['fname'] = '' fields['lname'] = '' fields['job'] = '' fields['uid'] = str(uid) create_people_row(uid, fields) context = {} # context info context['url'] = '/people/update/{}'.format(str(uid)) context['verb'] = 'post' # Actual http verb "delete" not in html4 context['fcnname'] = 'Update' context['viewonly_phrase'] = '' return template('people/crudform', r=fields, c=context) def main(): lst = list_people(people) bottle.run(debug=True, host=HOST, port=PORT, reloader=False) if __name__ == '__main__': main()
This app is for illustration only. Its database is an in-memory dictionary that gets restarted when the program is restarted. Its URLs aren't always accurate. It's as secure as a lace door. It wasn't that easy to write. But it does work, and it illustrates a CRUD web app using Bottle.
Here's why I even care about Bottle. Check out this Yaml file:
--- appname: General Template tables: - name: tableone dbname: tableone # Must match table name in database rowdict: table1row formsort: - uid - lname - fname - job listsort: - lname - fname - job - uid uidcolss: 0 columns: - abbr: uid formname: uid formlabel: uid listlabel: uid uid: True colvar: uid - abbr: lname colvar: lname formname: lname formlabel: Last name Listlabel: lname - abbr: fname colvar: fname formname: fname formlabel: First name Listlabel: fname - abbr: job colvar: job formname: job formlabel: Job Listlabel: job - name: tabletwo dbname: tabletwo # Must match table name in database rowdict: table2row formsort: - uid - lname - fname - job listsort: - lname - fname - job - uid uidcolss: 0 columns: - abbr: uid formname: uid formlabel: uid listlabel: uid uid: True colvar: uid - abbr: lname colvar: lname formname: lname formlabel: Last name Listlabel: lname - abbr: fname colvar: fname formname: fname formlabel: First name Listlabel: fname - abbr: job colvar: job formname: job formlabel: Job Listlabel: job
The preceding Yaml file captures all information necessary to lay out a CRUD app for each of several tables. Obviously in real life tabletwo would have different fields, but you get the idea. If I or somebody else writes a program to convert a Yaml file with the right information into a well crafted Bottle app, one could punch out a 4 table CRUD app in one day.