In lesson one, we learned about many of Python's basic constructs. In lesson two, we learned about building classes, and using code in the standard library. In this lesson, we will look at a few more standard library modules. We'll also get Python to generate us a web page using the CGI interface.
Contents
Here is the customer class from last week. Make sure you can explain all its parts. Make sure you can write a class on your own that can use all of the same features as these classes do:
#!/usr/bin/python
class customer(object):
company="Bluehost"
def __init__(self, name, domain):
self.name = name
self.domain = domain
def domain_expire(self):
message = "Hello, %s. Your domain name, %s, has expired." % \
(self.name, self.domain)
return message
def __str__(self):
return """Name: %s
Domain: %s
Company: %s
""" % (self.name, self.domain, self.company)
class hmcustomer(customer):
company="Hostmonster"
This module makes it extremely easy to add and parse command line options to our scripts.
Let's take a look at the example from the official docs:
#!/usr/bin/python
from optparse import OptionParser
parser = OptionParser()
parser.add_option("-f", "--file", dest="filename",
help="write report to FILE", metavar="FILE")
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose", default=True,
help="don't print status messages to stdout")
(options, args) = parser.parse_args()
#these two lines were added to show what's going on under the hood.
print options
print args
I put my code in a file called opt.py, and this is how I can use it:
jefferya@tam:~$ python opt.py
{'verbose': True, 'filename': None}
[]
jefferya@tam:~$ python opt.py -f nothing.txt
{'verbose': True, 'filename': 'nothing.txt'}
[]
jefferya@tam:~$ python opt.py -q -f nothing.txt
{'verbose': False, 'filename': 'nothing.txt'}
[]
jefferya@tam:~$ python opt.py -q
{'verbose': False, 'filename': None}
[]
jefferya@tam:~$ python opt.py -h
Usage: opt.py [options]
Options:
-h, --help show this help message and exit
-f FILE, --file=FILE write report to FILE
-q, --quiet don't print status messages to stdout
Now that we have optparse in our toolbelt, let's spice up the smtplib example from last time:
#!/usr/bin/python
import smtplib
def send(from_addr, to_addr, subject="Test", \
mail_body="This is a test message", mail_server="localhost", \
mail_server_port=25, authuser="", authpass="", verbosity=0):
from_hdr = 'From: %s' % from_addr
to_hdr = 'To: %s' % to_addr
subject_hdr = "Subject: %s" % subject
s = smtplib.SMTP(mail_server, mail_server_port)
s.set_debuglevel(verbosity)
ehlo_res = s.ehlo()
s.starttls()
if "AUTH PLAIN LOGIN" in ehlo_res[1] and authuser:
s.login(authuser, authpass)
else: print "not using authentication"
email_message="%s\n%s\n%s\n\n%s" % (from_hdr, to_hdr, subject_hdr, mail_body)
s.sendmail(from_addr, to_addr, email_message)
s.quit()
if __name__=="__main__":
import optparse
from optparse import OptionParser
parser = OptionParser(usage="usage: %prog -f ADDRESS -t ADDRESS [options]")
parser.add_option("-f", "--from", dest="from_addr", \
help="Senders address", metavar="ADDRESS")
parser.add_option("-t", "--to", dest="to_addr", \
help="Destination address", metavar="ADDRESS")
parser.add_option("-u", "--user", dest="authuser", \
help="SMTP authentication user", metavar="USERNAME", default=None)
parser.add_option("--pass", "--password", dest="authpass", \
help="SMTP authentication password", metavar="PASSWORD", default=None)
parser.add_option("-s", "--server", dest="mail_server", \
help="Mail server hostname", metavar="HOST", default="localhost")
parser.add_option("-p", "--port", dest="mail_server_port", \
help="Mail server port", metavar="PORTNUM", type="int", default=25)
parser.add_option("-v", "--verbosity", dest="verbosity", \
help="smtplib debug value", metavar="VAL", type="int", default=1)
(options, args) = parser.parse_args()
if not (options.from_addr and options.to_addr):
parser.error("You must supply both a sending and destination address.")
send(**eval(str(options)))
That's a lot of code to look at all at once, but just take a look one line at a time. Effectively, what this file, smtplibopt.py, is, is a hybrid. I can run it from the command line, and get a regular and complete set of features. I can also import it from the Python interpretor, or from other code, and use the send function that way too.
There is a cool piece of trickery going on here. The multiple assignment line: (options, args) = parser.parse_args() assigns an optparse.Values instance to options. This doesn't behave like a standard dictionary, but its __str__ method produces output identical to a dictionary definition. eval takes a string and treats it like actual Python code. In this case, it returns a dictionary, which we can then pass directly to send.
Let's try using this concoction from the command line:
jefferya@tam:~$ python smtplibopt.py -h
Usage: smtplibopt.py -f ADDRESS -t ADDRESS [options]
Options:
-h, --help show this help message and exit
-f ADDRESS, --from=ADDRESS
Sender address
-t ADDRESS, --to=ADDRESS
Destination address
-u USERNAME, --user=USERNAME
SMTP authentication user
--pass=PASSWORD, --password=PASSWORD
SMTP authentication password
-s HOST, --server=HOST
Mail server hostname
-p PORTNUM, --port=PORTNUM
Mail server port
-v VAL, --verbosity=VAL
smtplib debug value
jefferya@tam:~$ python smtplibopt.py
Usage: smtplibopt.py -f ADDRESS -t ADDRESS [options]
smtplibopt.py: error: You must supply both a sending and destination
address.
jefferya@tam:~$ python smtplibopt.py -f jefferya@programmerq.net -t
pinguino@box552.bluehost.com -u pinguino --pass my_password -s
box552.bluehost.com -v 0
This is basically a remote control for your web browser, whatever it may be.
Let's jump right in:
>>> import webbrowser >>> dir(webbrowser) ['BackgroundBrowser', 'BaseBrowser', 'Elinks', 'Error', 'Galeon', 'GenericBrowser', 'Grail', 'Konqueror', 'Mozilla', 'Netscape', 'Opera', 'UnixBrowser', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '_browsers', '_iscommand', '_isexecutable', '_synthesize', '_tryorder', 'get', 'main', 'open', 'open_new', 'open_new_tab', 'os', 'register', 'register_X_browsers', 'shlex', 'stat', 'subprocess', 'sys', 'time']
Let's make our customer.py helpful, by adding a cpm and cpanel function that will open up the respective tool for the customer's domain:
def cpm(self):
import webbrowser
webbrowser.open_new_tab("https://www.bluehost.com/" + \
"cgi/admin/user/cpanel/%s" % self.domain)
def cpanel(self):
import webbrowser
webbrowser.open_new_tab("https://www.bluehost.com/" + \
"cgi/admin/user/cpanel_login/%s" % self.domain)
This section assumes you are already familiar with CGI scripts. They can be written in any language. For info on the CGI spec, please see http://hoohoo.ncsa.illinois.edu/cgi/.
The CGI interface is a standard way of allowing a program to generate web pages, process form data, get information about the web server, etc. We'll implement a very simple Python CGI script that can output a simple page, issue a redirect, and even take POST data.
A quick test script that employs the CGI interface has proven useful to me many times:
#!/usr/bin/python print "Content-type: text/plain" print print "Hello World" import datetime print "The time now is:", datetime.datetime.now()
Because CGI scripts output any headers, you can easily redirect a browser:
#!/usr/bin/python print "Status: 303 See Other" print "Location: http://www.google.com/" print "Content-type: text/plain" print print "The requested page has moved. Please google your question instead."
POSTing to a CGI script can be a bit tricky. Post data is in one big long string that gets passed to the CGI script via standard input. Let's just regurgitate that data back to the user:
#!/usr/bin/python
print "Content-type: text/plain"
print
print "Here is your data:"
import sys
for i in sys.stdin.readlines():
print i
So, that has a gotcha in it. This script terminates after saying "Here is your data:" no matter if you have POSTed anything to it or not. POST data is passed in on stdin, but it includes no newlines, and no EOF character. readlines only returns something if it can tell where either a newline, or an EOF is. The CGI spec was designed with a way to tell the cgi script how much data there is. If the script reads too little data, then some would be lost. If it tries to read too much, it would be stuck waiting for more input to come in on stdin.
The CGI script has many environmental variables that are set, so the script can get information about how and where it is being run. One such variable is called CONTENT_LENGTH. We can read exactly the right number of bytes if we look up this variable:
#!/usr/bin/python print "Content-type: text/plain" print print "Here is your data:" import sys, os print sys.stdin.read(int(os.environ['CONTENT_LENGTH']))
You can take any HTML form, and use it to POST to this script, and see what the webserver receives. This version works.
We've implemented a couple things using very simple CGI. We have access to all sorts of things that we can do by hand, but sometimes it's just silly to do things by hand. We even ran into a snag when trying to process POST data. When a project gets larger than one or two screens worth of code, abstracting this mundane stuff makes things much more manageable and readable. The cgi module is designed to help you do just this.
Let's start off with some nice form processing:
#!/usr/bin/python
print "Content-type: text/html"
print
import cgi
form = cgi.FieldStorage()
if not (form.has_key("name") and form.has_key("addr")):
print "<H1>Error</H1>"
print "Please fill in the name and addr fields."
return
cgi.print_form(form)
All this form does is create a FieldStorage instance. This class is smart enough to look and see that POST or GET data is present, and parse it into something a bit more useful. It checks to make sure that there is a key called "name" and a key called "addr". If either is not present, it complains.
There is another module called cgitb. The cgi module docs urge the reader to implement the cgitb module. cgitb takes any Exceptions that Python may encounter during the execution of the script, and formats it into HTML. This makes debugging less painful. You can also use cgitb to do something else with the Traceback, like save it to a log file or e-mail it. It's generally a bad idea to expose Traceback information to a hostile network like the Internet.
Let's modify our previous example a bit:
#!/usr/bin/python
print "Content-type: text/html"
print
import cgi, cgitb
cgitb.enable()
form = cgi.FieldStorage()
if not form.has_key("port"):
print "<H1>Error</H1>"
print "Please fill in the port field."
return
port = int(form['port'].value)
cgi.print_form(form)
I've changed this to look for a "port" key instead of "name" and "addr" keys. It then tries to store an integer version of the "port" value to a Python variable called port. As long as the user submits a valid integer as the value for the "port" field, then the code will execute just fine. If they submit something silly, like letters, or a decimal, the casting to an int will fail and raise an exception. Without cgitb this would have simply failed with a 500 Internal Server error, and absolutely no helpful output.
Try posting to this script with "port" set to "twenty" and again set to "20" to see how it behaves.
cgitb makes it very nice and very easy to diagnose things during application development. Most new web applications don't implement just plain old cgi interfaces anymore, but almost all other interfaces are based on cgi to some degree.
Your homework is to write a cgi script in Python that takes form input, and displays it back to the user in nicely formatted HTML.
If you feel really ambitious, make a form mailer script using cgi, cgitb, and smtplib.
If you have any questions, please post them to the mailing list on the Google Group.
I'm also happy to answer questions in person. This document will also be updated to reflect questions and concerns that come up both in person and on the mailing list.
Note
License and Legal
This document is Copyright (c) Jeff Anderson. It is not freely distributable. It is intended to be distributed amongst employees of Bluehost for the purpose of learning the Python language. Any copying or distribution beyond this is not permitted. These terms may be modified at any time by the author.
This notice applies only to the content. The HTML code generated by docutils is in the public domain. The syntax highlighting Javascript is google-code-prettify.