Bobo Documentation and Tutorial Available 
Author Message
 Bobo Documentation and Tutorial Available

Hello Amos,

Quote:
> http://www.*-*-*.com/

Great page -- but unreadable on non-windows computers:
Too many
  <FONT FACE="Arial" SIZE=2>
Tags. The font gets soo small that I cannot read it on
my SUN with Netscape.

I attached a 'plain html' version of the document...
(I just removed all <FONT> tags)

If you like it, replace it...

Greetings,

Michael
--
     ''''\     Michael Scharf

    `    >     http://www.*-*-*.com/

[ index.html 50K ]

An Introduction to Bobo, the python Object Publisher

This introduction to Bobo was written to for Ken McDonald's forthcoming book, Quick Python Book from Manning Publications. The material here is a draft of a chapter of the book. We are reproducing this material here with Ken McDonald's kind permission.

For more information about Bobo check out the Bobo Homepage and the Bobo mailinglist archives.

Bobo Basics

Publishing dynamic data on the Web can be unnecessarily difficult. Even with excellent tools like Python's cgi module, a Web developer must be familiar with the many technical details of HTTP, CGI and other Web protocols. Worse, these details often end up in code, reducing clarity and code reuse.

As Web applications become more complex, problems due to the non object-oriented nature of CGI become apparent. Typical Web applications consist of a number of single-purpose CGI scripts which have no standard way to share information or specify other objects in the system. Manageability becomes a problem very quickly, and many of Python's object-oriented benefits are lost in such a system.

Bobo, also known as the Python Object Publisher, is a collection of technologies for developing powerful, coherent and maintainable Web applications with Python. It allows collections of Python objects to be published as World Wide Web resources without CGI or HTTP specific code.

Bobo allows you to write a Web application in the same way that you write any normal Python program-you write classes that model the problem domain, and create objects to actually do the work. Bobo does the work of handling incoming Web requests, finding and calling your objects, and returning the results. Because they contain no CGI-specific code, these applications can be easily moved from one publishing mechanism, such as CGI, to other mechanisms such as FastCGI or CORBA without change.

Creating a Bobo application is simply a matter of creating a regular Python module containing the objects to be published, and placing this module along with some Bobo-provided support files in the appropriate place on a Web server. (The details of this process are covered in the documentation for the Bobo package.)

Object Publishing

Using Bobo, normal Python objects are published to the Web. The Web server "calls" the objects in much the same way that other Python objects would. Bobo works by wrapping a Python module and providing all of the plumbing necessary to turn HTTP requests into object calls. When an incoming HTTP request is received, Bobo handles the mapping between HTTP data and Python objects and performs the following functions:

  • searches the object space for the object referenced in the incoming request URL
  • marshals incoming form data, cookies and request data to object parameters
  • performs authorization and authentication of the incoming request based on authorization data gathered while searching for the object
  • invokes the object
  • converts the invocation return value or exception value to an HTTP response and returns this to the Web server

Bobo does all of the HTTP and CGI drudgery, allowing the developer to write simpler code. In many ways, Bobo functions as an Object Request Broker (ORB). Published modules do not need to handle receiving and decoding the HTTP request, nor formatting and sending the HTTP response.

Object traversal: from URLs to object calls

Bobo effectively joins the namespace of the Web with the namespace of a Python module.

Take the example module Hello.py:

"""A simple Bobo application"""
  def sayHello(name='World'):
	  """Display a greeting"""
	  return "Hello %s!" % name

From the interactive Python interpreter, you can interact with this module and call the sayHello function:

>>> import Hello

>>> Hello.sayHello()

'Hello World!'

>>> Hello.sayHello("Billy")

'Hello Billy!'

Publishing the module with Bobo allows you to perform the same interaction via the Web. Simply place this module on a Web server with the appropriate Bobo support files, and it becomes a fully functional Web application. Bobo allows you to call the sayHello function with a URL that Bobo maps into the Hello module's namespace.

For example, sending your Web browser to http://www.*-*-*.com/

is equivalent to:

>>> import Hello

>>> Hello.sayHello()

Both Bobo and the interactive interpreter return the text:

'Hello World!'

Sending your Web browser to http:// www.mydomain.com/cgi-bin/Hello/sayHello?name=Billy

is equivalent to:

>>> import Hello

>>> Hello.sayHello(name="Billy")

Both Bobo and the interactive interpreter return the text:

'Hello Billy!'

Once you set up Bobo to publish the Hello module, sending your browser to URLs is just like calling objects in the published module's namespace. The URL is used by Bobo to determine which object to call within the module and what arguments to pass to the called object. In other words,

/published_module/object1/object2/meth?arg1=value1&arg2=value2

is equivalent to:

>>> published_module.object1.object2.meth(arg1="value1", arg2="value2")

Bobo performs object traversal by converting steps in the URL path to getattr or getitem calls. Bobo publishes the object that it comes to at the end of the URL.

So object publishing with is really pretty simple. How about what happens when you POST to Bobo with Web form? Exactly the same thing happens. Suppose you have this form:

<form action=" http://www.*-*-*.com/ " method="post">

Arg1: <input type="text" name="arg1">

Arg2: <input type="text" name="arg2">

<input type="submit">

</form>

Submitting the form will call the published module in exactly the same way as the previous example. So using this form with Bobo is also equivalent to:

>>> import published_module

>>> published_module.object1.object2.meth(arg1="value1", arg2="value2")

However, in the case of the Web form, "value1" and "value2" are determined by what you type into the form inputs.

Object Publishing Details

Bobo converts URLs to object calls in a controllable way. Bobo enforces rules that allow you to control which objects are accessible from the Web. Objects that are accessible from the Web via Bobo are known as "published" objects. The objects defined in a module are published if they:

  • can be found in the module's global name space
  • do not have names starting with an underscore
  • have non-empty documentation strings
  • are not modules

Sub-objects (or sub-sub-objects, ...) of published objects may also be callable from the Web. Applications can publish whole hierarchies of objects that provide a much richer set of services than common CGI scripts, and are far easier to maintain. Sub-objects are published if they:

  • have non-empty documentation strings
  • have names that do not begin with an underscore
  • are not modules

Object methods are considered to be sub-objects, and may be published as well. When Bobo publishes a function or method, it figures out what arguments are expected, and attempts to find values to pass based on argument names in the function or method's signature. Argument values may come from:

  • Form input fields
  • HTTP headers
  • HTTP cookies
  • CGI environment variables
  • Special values defined by Bobo

The REQUEST and RESPONSE objects provided by Bobo allow you the same level of control over request processing as a raw CGI script, but they present a much nicer object interface.

The return value of an object invocation is returned as the HTTP response. When developing Web applications, output to browsers is nearly always in the form of text, especially HTML. By default, Bobo assumes that output is in text format. It will automatically detect whether output is plain text or HTML, though an application may also set the return content type explicitly.

Bobo also provides a number of facilities to aid in debugging applications. Here's how Bobo deals with any unhandled exceptions that may be raised by an application:

  • Bobo prevents traceback listings from being directly displayed and from causing server errors.
  • Bobo (minimally) attempts to convert Python exceptions to HTTP exceptions and automatically sets exception status codes.
  • Bobo automatically generates error pages using exception type and value information, including traceback information in HTML comments.

The most basic debugging tool is traceback information provided in error outputs. For example, suppose we introduce an error in a published method. Instead of the inscrutable "Server Error" you would get with normal CGI, Bobo will return HTML containing an embedded Python traceback that allows you to pinpoint the error:

<html>

<head>

<title>AttributeError</title>

</head>

<body>

Sorry, an error occurred.<p>

<!--

Traceback (innermost last):

File ads.py, line 15 in image_type

AttributeError: spam

-->

</body>

</html>

A Simple Example

A common type of Web application is the "feedback form". Usually, the Web user is presented with a form that is filled out and then submitted to a CGI script on the server. The CGI script generally collects the data submitted in the form into some sort of report which is either stored or emailed to a Web site administrator.

Let's look at a Bobo implementation of the feedback application. First, we need an HTML file for our feedback form:

<HTML>

<HEAD><TITLE>Feedback Form</TITLE></HEAD>

<BODY>

<P>

<FORM ACTION="/cgi-bin/feedback/save_feedback" METHOD="POST">

Type of feedback:

<SELECT NAME="feedback_type">

<OPTION> Compliment

<OPTION> Criticism

</SELECT>

</P>

<P>

Your comments:

<TEXTAREA NAME="comments"></TEXTAREA>

</P>

<INPUT TYPE="SUBMIT" VALUE="Submit Feedback">

</FORM>

</BODY>

</HTML>

Next, we create our feedback application, feedback.py:

"""feedback.py: A simple feedback form application"""
def save_feedback(feedback_type, comments):
	"""A function to handle submitted feedback form data"""
	# Open a file for appending to save the feedback,
	# creating the file if necessary.
	try: file=open("feedback_data", "a")
	except IOError:
		file=open("feedback_data", "w")
	# Save the data
	file.write("Feedback type: %s\n" % feedback_type)
	file.write("Comments: %s\n\n" % comments)
	file.close()
	return "Thank you for your feedback!"

That's all there is to it. When a Web user submits the feedback form, Bobo finds the target of the request, our save_feedback function. It finds that save_feedback takes two arguments, "feedback_type" and "comments". These two values are found in the form data submitted in the request, and passed as the arguments to save_feedback when Bobo calls the function. The save_feedback function simply appends the data to a file, and the return value is then sent back to the Web user.

Perhaps you later decide that you also want a version of the feedback application that emails the Web user's comments to a site administrator. To do this, you would simply add another function to feedback.py:


Basic Bobo examples: Guest Books and Ad Generators

Bobo can do everything normal CGI scripts can do, only better. Here are some basic examples of how to perform normal CGI scripting tasks with Bobo.

One of the nice things about using Bobo is that you can provide a lot of functionality without having to deal with an ever-growing number of separate CGI scripts. If you need new functionality in your Bobo application, you simply add new objects. This program demonstrates a simple Web-based "guestbook", which generates a form for Web user, handles submissions of that form, and acknowledges successful submissions.

guestbook.py

"""Module guestbook: a simple guestbook application"""

class GuestBook:
  """A guestbook object that provides both the forms
     and the handling of submitted form data."""

  def __init__(self, title, filename):
    self.title=title
    self.filename=filename

  def guestbookForm(self):
    """Return the guestbook form to the user"""
    return """<HTML>
              <HEAD><TITLE>%s</TITLE></HEAD>
              <BODY>
              <H2>%s</H2>
              Please sign our guestbook!
              <P>
              <FORM ACTION="signGuestBook" METHOD="POST">
              Name: <INPUT TYPE="TEXT" NAME="name"><BR>
              Email: <INPUT TYPE="TEXT" NAME="email"><BR>
              <INPUT TYPE="SUBMIT" VALUE="Sign Guestbook">
              </FORM>
              </BODY>
              </HTML>""" % (self.title, self.title)

  def successPage(self):
    """Return a page to thank the user on success"""
    return """<HTML>
              <HEAD><TITLE>%s</TITLE></HEAD>
              <BODY>
              <H2>Thank you!</H2>
              Thank you for signing %s!
              </BODY>
              </HTML>""" % (self.title, self.title)

  def signGuestBook(self, name, email='not specified'):
    """Handle a submitted guestbook form"""

    # Open a file to save the guestbook entry
    try: file=open(self.filename, 'a')
    except IOError:
        file=open(self.filename, 'w')
    entry='Guestbook entry: %s %s\n' % (name, email)
    file.write(entry)
    file.close()
    return self.successPage()


# Create an instance of a GuestBook
myGuestBook=GuestBook('My GuestBook', 'guestbookdata.txt')

Notice in the GuestBook example that the GuestBook method that handles form submissions (signGuestBook) defines email as a keyword argument. If the user does not enter an email address in the form, the method will simply use the default value for email defined in the method signature. The name argument, however, is a required argument-if Bobo fails to find a name field in the submitted form data (or elsewhere in the HTTP request), an html error message will automatically be returned to user informing him that he forgot to fill in the name field. A normal CGI script would have to manually verify that all expected form fields were submitted and handle the chore of returning a meaningful error message to the Web user.

Normally, Bobo copies values from form fields as strings, but Bobo can be told to perform type conversions automatically by including type information in HTML form variable names.

Lets look at a version of the GuestBook application that makes use of automatic type conversion. Notice that the HTML returned by the guestbook Form method now contains a few extra form fields whose name attributes now contain Bobo type conversion information.

 

guestbook2.py

"""guestbook2: a simple guestbook application"""

class GuestBook:
  """A guestbook object that uses automatic Bobo
     type conversion in its Web form"""

  def __init__(self, title, filename):
    self.title=title
    self.filename=filename

  def guestbookForm(self):
    """Return the guestbook form to the user"""
    return """<HTML>
              <HEAD><TITLE>%s</TITLE></HEAD>
              <BODY>
              <H2>%s</H2>
              Please sign the guestbook!
              <P>
              <FORM ACTION="signGuestBook" METHOD="POST">
              Name: <INPUT TYPE="TEXT" NAME="name:required"><BR>
              Age: <INPUT TYPE="TEXT" NAME="age:int"><BR>
              Email: <INPUT TYPE="TEXT" NAME="email:required"><BR>
              Which sports do you like? Check all that apply:
              <INPUT TYPE="CHECKBOX" NAME="sports:list" VALUE="baseball"> Baseball <BR>
              <INPUT TYPE="CHECKBOX" NAME="sports:list" VALUE="football"> Football <BR>
              <INPUT TYPE="CHECKBOX" NAME="sports:list" VALUE="basketball"> Basketball <BR>
              <INPUT TYPE="CHECKBOX" NAME="sports:list" VALUE="golf"> Golf <BR>
              <INPUT TYPE="SUBMIT" VALUE="Sign Guestbook">
              </FORM>
              </BODY>
              </HTML>""" % (self.title, self.title)

  def successPage(self):
    """Return a page to thank the user on success"""
    return """<HTML>
              <HEAD><TITLE>%s</TITLE></HEAD>
              <BODY>
              <H2>%s</H2>
              Thank you for signing!
              </BODY>
              </HTML>""" % (self.title, self.title)

  def signGuestBook(self, name, age, email, sports):
    """Handle a submitted guestbook form"""

    # Open a file to save the guestbook entry
    try: file=open(self.filename, 'a')
    except IOError:
        file=open(self.filename, 'w')

    # sports will be passed as a list
    num=len(sports)

    entry='Guestbook entry: %s %s age %d, likes %d sports.\n' % (name, email, age, num)
    file.write(entry)
    file.close()
    return self.successPage()


# Create an instance of a GuestBook
myGuestBook=GuestBook('My GuestBook', 'guestbookdata.txt')

In the form generated by the guestbookForm method, the input tag for the "age" field is named "age:int". The ":int" part of the name indicates to Bobo that the field should be converted to an integer before passing it to the published method. The guestbookForm HTML now contains a sequence of CHECKBOX fields which allow the user to select his favorite sports. By specifying the name of these fields as "sports:list", Bobo can automatically convert the form data relating to the "sports" fields into a list containing all of values selected by the user. Finally, we specified ":required" for both the "name" and "email" fields. This lets Bobo know that these fields must be non-empty strings.

Other type conversions supported by Bobo include float, long, tuple and regex. If a value entered in a form does not match a type specification, or if a required form value is omitted, Bobo automatically generates an error response.

All of this automation is great, but what if you need to do more advanced things like set HTTP cookies or return data of a type other than text or HTML? For every request handled, Bobo creates a REQUEST and a RESPONSE object that provide a nice object interface for performing these kinds of tasks. The REQUEST and RESPONSE objects can be used by an application by simply including them in the method signature of a published method.

This program demonstrates the use of the REQUEST and RESPONSE objects. It is a simple random Web ad generator whose URL can be called from HTML pages (or Bobo-generated HTML) to insert an ad into the page.

ads.py

"""ads: a simple random ad generator"""

class AdGenerator:
  """A random ad generator object. It is passed the name
     of a directory containing ads - files in .gif format"""

  def __init__(self, ad_dir):
    self.ads_dir=ad_dir

  def random_ad(self, RESPONSE):
    """Return a random ad"""
    import os, whrandom

    # Select a random ad file
    ad_list=os.listdir(self.ad_dir)
    ad_name=whrandom.choice(ad_list)
    ad_name=os.path.join(self.ad_dir, ad_name)

    # Open ad file, using 'rb' to open it in binary mode!
    ad_file=open(ad_name, 'rb')
    ad_data=ad_file.read()
    ad_file.close()

    # Set the content-type of the response
    RESPONSE.setHeader('content-type', 'image/gif')
    return ad_data


# Create an instance of the ad generator,
# passing the directory where ads are kept

generator=AdGenerator('/www/ads/gifs')

Returning non-text data is as simple as including the RESPONSE object in the method signature and setting the correct content-type. A variation on the random ad generator demonstrates using both the REQUEST and RESPONSE objects. This new version uses HTTP cookies to avoid showing a client the same ad twice in a row, and keeps a log of the names of remote computers that access ads. It can also return ads in both .gif and .jpg format.

ads2.py

"""ads2: a fancier random ad generator"""

class AdGenerator:
  """A random ad generator object. It is passed the name
     of a directory containing ads - files in .gif format"""

  def __init__(self, ad_dir):
    self.ads_dir=ad_dir

  def image_type(self, filename):
    # This method is not published

    typemap={'.gif' : 'image/gif', '.jpg' : 'image/jpeg'}
    ext=filename[-4:]
    return typemap[ext]

  def log_info(self, remote_host, user_agent)
    # This method is not published

    try: log=open('log.txt', 'a')
    except IOError:
      log=open('log.txt', 'w')
    line='Host: %s, Browser: %s\n' % (remote_host, user_agent)
    log.write(line)
    log.close()

  def random_ad(self, REQUEST, RESPONSE, last_ad_cookie=''):
    """Return a random ad"""
    import os, whrandom

    # Log some info about this request
    self.log_info(REQUEST['REMOTE_HOST'], REQUEST['HTTP_USER_AGENT'])

    # Select a random ad file
    ad_list=os.listdir(self.ad_dir)
    ad_name=whrandom.choice(ad_list)
    ad_name=os.path.join(self.ad_dir, ad_name)

    # Make sure we dont send the same ad twice in a row
    if last_ad_cookie:
      while (last_ad_cookie == ad_name)
        ad_name=whrandom.choice(ad_list)
        ad_name=os.path.join(self.ad_dir, ad_name)

    # Determine the ad type
    ad_type=self.image_type(ad_name)

    # Open ad file, using 'rb' to open it in binary mode!
    ad_file=open(ad_name, 'rb')
    ad_data=ad_file.read()
    ad_file.close()

    # Set a cookie containing the name of the ad served this time
    RESPONSE.setCookie('last_ad_cookie', ad_name)

    # Set the content-type of the response
    RESPONSE.setHeader('content-type', ad_type)
    return ad_data


# Create an instance of the ad generator,
# passing the directory where ads are kept

generator=AdGenerator('/www/ads/gifs')

Advanced Bobo

As we've seen it's quite easy to get started with Bobo. But what happens when your project grows? Bobo provides many advanced features that help you in significant Web application projects. Perhaps, more importantly, Bobo gets out of your way, and frees you to write good clean object-oriented code, which can significantly improve your development process on large projects. Bobo allows you to get started simply, but provides facilities for advanced work when you are ready for them. Large CGI applications are notoriously fragile, but Bobo allows you to avoid these clumsy structures.

Let's explore a couple of Bobo's advanced features.

HTML generation with DocumentTemplate

One common problem when writing Web applications is the mutual dependence of code and HTML. This interdependence makes it difficult to change the look and feel of a Web application without mucking around in the code. This problem frustrates both programmers and designers. Bobo's DocumentTemplate package helps solve this problem by allowing an object's HTML representation to be defined outside the code. Using DocumentTemplate, HTML templates may be edited by people who know HTML and don't know Python, while associated Python code may be edited by people who know Python but not HTML.

DocumentTemplates provide for creation of textual documents, such as HTML pages, from template source by inserting data from Python objects and namespaces. However, there is nothing Web specific about DocumentTemplate; it can be used for any Python text generation project. Here's a simple DocumentTemplate:

my_template.dtml

My name is <!--#var name--> and I am <!--#var age--> years old.

Here's a simple program that uses this template:

# create a template from a text file
#
template=DocumentTemplate.HTMLFile("my_template.dtml")

# render the template
#
print template(name="Billy", age=77)

When you run the program it will print:

My name is Billy and I am 77 years old.

As you can see, DocumentTemplate tag syntax is similar to that of server side include (SSI) tags commonly used by Web servers. There are many special DocumentTemplate tags, but the most basic one is the var tag. A var tag searches for a variable and inserts its value into the template text. In the case of our example we specified the variables name and age as key word arguments when we called the template instance. DocumentTemplates can also locate variables in objects by searching for object attributes and calling object methods. Let's look at an example:

class Person:
	"A simple person"
	def __init__(self,name,age):
		self.name=name
		self.age=age

# create a Person instance		
#
bill=Person("Billy",77)

# create a template from a text file
#
template=DocumentTemplate.HTMLFile("my_template.dtml")

# render the template
#
print template(bill)

When you run the program it will print:

My name is Billy and I am 77 years old.

This is all fine and good you may say, but what does this have to do with Bobo, and object publishing? Well, one important thing about DocumentTemplates is that they masquerade as functions, so Bobo will call templates that are stored as attributes of published objects. When publishing the DocumentTemplate, Bobo will pass the object in which the template was found and the HTTP request object as arguments.

Let's update our example one more time to demonstrate this principle:

class Person:
	"A simple person"
	def __init__(self,name,age):
		self.name=name
		self.age=age
	
	# document template class attribute
	# which masquerades as a method	
	index_html=DocumentTemplate.HTMLFile("my_template.dtml")
	
bill=Person("Billy",77)

Now when Bobo publishes the index_html "method" of the bill object it will return:

My name is Billy and I am 77 years old.

This is because Bobo calls bill's DocumentTemplate with the bill object and the request as arguments:

>>> bill.index_html(bill,REQUEST)

Note that the DocumentTemplate is a class attribute, which is shared by all Person instances. If you change the template file (my_template.dtml) then the rendering of all Person instances will change accordingly, and you don't need to change any Python code. Another thing to note is that when Bobo calls a DocumentTemplate, it passes a REQUEST object to the template that encapsulates the HTTP request information. This means that your templates can reference HTTP request information like this:

You just came from this URL: <!--#var HTTP_REFERER-->
And this is the web browser you are using: <!--#var HTTP_USER_AGENT-->

Now let's look at a few more DocumentTemplate features.

More DocumentTemplate tags: conditionals, sequences, and expressions

DocumentTemplate supports the conditional insertion of text. This means you can perform if-then types of comparisons within your templates. Here's a simple example:

<!--#if old-->I am old.<!--#/if-->

If this template is passed a variable named "old" with a true (not 0, "" or None) value, then the template will display:

I am old.

You can also add an else tag to provide for more powerful condition testing:

<!--#if HTTP_REFERER-->
You came to this page from <!--#var HTTP_REFERER-->.
<!--#else-->
You came to this page directly.
<!--#/if-->

This template looks for an HTTP_REFERER variable in its namespace, and returns an appropriate message.

Another feature of DocumentTemplate is to loop over sequences. Let's see how this is done:

people_template.dtml

<!--#in people-->
name: <!--#var name--> age: <!--#var age-->
<!--#/in-->

This template will loop over a sequence of objects and print the name and age of each object. Here's an example of how we might use this template:

class Person:
	"A simple person"
	def __init__(self,name,age):
		self.name=name
		self.age=age

# define a sequence
#		
my_family=[Person("Billy",77),Person("Pearl", 68),Person("Willard",72)]

# define a template
#
people_template=DocumentTemplate.HTMLFile("people_template.dtml")

# render the template
#
print people_template(people=my_family)

This program will print:

name: Billy age: 77

name: Pearl age: 68

name: Willard age: 72

DocumentTemplate supports many advanced features of sequence insertion including displaying large sequences in batches.

The last feature of DocumentTemplate that we will look at here is the ability to insert snippets of Python code in templates. This trick is performed with variable expressions. Here's a simple example:

I am <!--#var expr="age*7"--> years old in dog years.

This template uses the expr feature of the var tag to multiply the variable "age" by 7 and display the results. This is a very powerful feature of DocumentTemplate and should be used with care. If you find yourself writing very complex expr code, you should probably write a method or function to perform the calculation and call the method or function from your template using a normal var tag instead. Here's a more complex example of how to create an HTML select tag with expr:

import DocumentTemplate

# define the template
#
select_template=DocumentTemplate.HTML("""\
<select>
<!--#in choices-->
<option<!--#if expr="_['sequence-item']==chosen"--> selected<!--#/if-->><!--#var sequence-item-->
<!--#/in-->
</select>""")

# render the template
#
print select_template(choices=[1,23,55,65,78,99],chosen=55)

When you run the program it prints:

<select>

<option>1

<option>23

<option selected>55

<option>65

<option>78

<option>99

</select>

In this example the DocumentTemplate builds the select tag by looping through the sequence variable "choices". For each item, it compares the current item (a special variable named "sequence-item") to the variable "chosen" if they are equal, then "selected" is printed.

Object Persistence with BoboPOS

After publishing collections of objects with Bobo, you will soon want a system for saving your objects and restoring them between requests. The Bobo Persistent Object System (BoboPOS) provides a solution to this problem. BoboPOS is a persistent-object system that provides transparent transactional object persistence to Python applications. BoboPOS, like DocumentTemplate is not just applicable to Web applications. BoboPOS is based on Python's standard object serialization functionality provided by the pickle module. BoboPOS takes this basic functionality several steps further by allowing for such advanced features as transactions, lazy object activation, object versioning, object reference management. Luckily you don't need to understand any of the{*filter*}details to make your objects persistent, and have them work with Bobo.

To get started with BoboPOS you simply create a PickleDictionary to act as an object store and place your objects in it. Also you must make sure your objects inherit from the mix-in Persistent class, and obey some simple conventions. Using BoboPOS is similar to using shelve for object persistence. Here's a complete example of creating and storing a persistent object with BoboPOS:

import BoboPOS

class Person(BoboPOS.Persistent):
	"A persistent person"
	def __init__(self,name,age):
		self.name=name
		self.age=age

# define the object store
#
object_store=BoboPOS.PickleDictionary("people_store.db")

# retrieve billy from the object store
#
if object_store.has_key("billy"):
	# if billy is in the object store, then
	# load it from the object store
	#
	billy=object_store["billy"]
else:
	# since billy isn't already in the object
	# store, we must create the object and 
	# place it in the object store.
	#
	billy=object_store["billy"]=Person("Billy",77)
	
	# normally bobo will handle transactions,
	# but in this special case, we need to 
	# commit the transaction to save the new
	# persistent object
	#
	get_transaction().commit()

# display the object that we retrieved
# from the object store
#
print billy.name, billy.age

This looks complex, but let's see what's going on here. First we have the definition of a persistent Person class which inherits from Persistent. Then we define the object store, which is a PickleDictionary object. Note that a PickleDictionary works just like a normal dictionary, you put and get items with the same syntax you would use for putting and getting items from a normal dictionary. Next comes the tricky part of the program where it not only loads an object from the database, but it also checks to see if the object exists, and creates it if necessary. The first time you run the program the object store will be created and the billy object will be stored in it. On later executions, the database will already exist, and it will already contain the billy object, so the program will merely load billy from the object store.

After creating the PickleDictionary and placing objects in it, object persistence becomes almost totally transparent. Bobo will handle transactions for you when your persistent objects change. If the name or age attributes of the billy object are changed in the course of a Web request, the new state of the billy object will automatically be saved. Another important feature is that sub-objects of persistent objects may be created or changed and they are automatically saved by BoboPOS. So for example if billy gets a son:

Billy.son=Person("Dan",55)

The sub-object will automatically be persistent. In fact, billy.son could contain more sub-objects, all of which would automatically be saved and restored as needed by BoboPOS. Bobo acts as the transaction manager and automatically starts and commits the transaction at the start and end of an HTTP request. If an error occurs during the request Bobo aborts the transaction, so your persistent objects are never left stranded in an inconsistent state.

Because of the ease of creating persistent object hierarchies, a common Bobo practice is to create a PickleDictionary with only a few root objects. The rest of the published objects are created as sub-objects of the root persistent objects. Not only is this arrangement convenient for BoboPOS, but Bobo itself, excels at traversing objects by mapping URLs to object hierarchies.

To recap here's a quick recipe for using BoboPOS:

  • Create persistent objects by subclassing BoboPOS.Persistent
  • Create a PickleDictionary and store all you persistent objects directly in it, or indirectly in it as sub-objects.
  • Create sub-objects of Persistent objects which are themselves Persistent or are used immutably.
  • Bobo will automatically handle transactions and save and restore your persistent objects as needed.

 

Even more features

Bobo is a powerful collection of tools and techniques for Web application development in Python. In this chapter we have only skimmed the surface of major Bobo components. Though you don't need to know about and understand all aspects of Bobo to use it effectively, it is tremendously scalable. As you begin to do more advanced things, it is likely that Bobo will already have features to help you.

Bobo can provide access control for your published objects by performing automated authentication and authorization. Bobo access control works with hierarchies of Web objects to provide a flexible way of controlling access to your resources. You can assign access roles to published objects and construct user databases to map Web users to access roles.

Bobo call interface, or BCI is a utility which allows remote procedure calls (RPC) over HTTP with objects published by Bobo. BCI allows you to write Python programs that access Web objects as if they were local functions or objects with methods. Not only can you call remote objects, but you can receive the results of the call as real Python objects.

One of the great things about Bobo is that it hides the publishing plumbing. Because of this, it is easy to switch publishing protocols easily without changing your published module. One of the most popular reasons to switch plumbing is to go from CGI to a form of long running process. A long running process is a server application that doesn't launch and exit with each HTTP request like a CGI program, but instead starts once and services many requests. A long running process in general performs much more efficiently and quickly than a process that starts and stops with every request. There are a number of ways to use long running processes with Bobo, including FastCGI, Persistent CGI, and Web server-modules.

One of Bobo's most valuable resources is its active community of users and developers. Python developers are increasingly turning to Bobo to implement their Web applications, and Bobo has been used as the foundation for a number of commercial products. The primary organ of this community is the Bobo mailing list. On this list you can exchange questions, answers and change proposals with many Bobo users and even the authors of Bobo. The Bobo mailing list is archived at:

http://www.*-*-*.com/

Advanced Bobo Example: Web Job Board

Since building Web applications with Bobo can be complex, we have provided you a with a complete working application to show how its done. The Web Job Board is a system for posting job openings on-line. It demonstrate how Bobo, DocumentTemplate and BoboPOS can work together to form a coherent Web application. The Job Board consists of a number of files:

  • JobBoard.py - The module that contains the JobBoard and JobLising classes.
  • job_app.py - The module published by Bobo.
  • job_board.dtml - A DocumentTemplate file used by the JobBoard class to display itself.
  • job_listing.dtml - A DocumentTemplate file used by the JobListing class to display itself.
  • job_add.dtml - A DocumentTemplate file used by the JobBoard class to gather information to create a new JobListing object.
  • confirm.dtml - A DocumentTemplate file used by the JobBoard class to give the user confirmation.

In addition, when the application is run it will create a job_board.db file to store the persistent objects.

Let's take a look at these parts and see how they work together for form a complete Web application. The most import part of the job board application is the JobBoard.py module that defines the JobBoard and the JobListing classes. The JobListing class describes a job-what the job title is, a description of the job, and when the job listing closes. The JobBoard class is just a collection of JobListing objects along with some methods for adding new job listings and getting rid of old job listings. Notice that DocumentTemplate objects are used to create the Web interface methods of the classes. Notice also that both classes are persistent, so that they can be stored in a BoboPOS object store.

Lets look at how Bobo will map URLs to these objects. A JobBoard object functions as a mapping object and holds JobListing objects. For example:

# my_board is a JobBoard instance
# my_listing is a JobListing instance
my_board["listing"]=my_listing

If for example, the URL of the my_board object is http://www.*-*-*.com/ ://www.mydomain.com/cgi-bin/board/listing.

This is the simple magic of Bobo at work. Given the URL of a root object it is easy to know the URL of its sub-objects. Our application takes advantage of this by never using absolute URLs, but merely using relative URLs to create hyperlinks from the JobBoard object to JobListing objects and back. To facilitate this process, each JobListing object has an id attribute which should be the same as the its key in the JobBoard mapping object. So in effect, every JobListing object knows its own URL.

You may also notice that there is very little Web-specific code in the JobBoard.py file. This arrangement will make it much easier to maintain your application as it grows and changes. This arrangement also makes it easier to give your application an HTML facelift without messing up its guts.

Here are some screen shots of what the Web Job Board looks like in action. Notice how the URLs map to methods of the JobBoard and JobListing objects. In these screen shots, the base URL of the Web Job Board is http://locahost/scripts/jobboard.py

This screen shot shows the JobBoard object's default method, index_html.

 

 

This screen shot shows a JobListing object displaying itself with its index_html method. Notice how the URL contains the id of the JobListing object which in this case is "902816029"

 

 

This screen shows the form created by the JobBoard object's add_html method. This form will call the JobBoard's add_listing method when the user submits the form.

 

 

This is the confirmation screen that is returned by the JobBoard object after a new JobListing is created by calling its add_listing method.

 

JobBoard.py

"""JobBoard - An example Bobo application

JobBoard.py  - The module which defines the job listing
               and job board classes
"""

from DocumentTemplate import HTMLFile
from BoboPOS import Persistent
from BoboPOS.PersistentMapping import PersistentMapping
import time

# location of dtml files
dtml_loc="D:\\Bobo\\JobBoard\\"

class JobBoard(PersistentMapping):
	"""A collection of job postings"""
	
	def __init__(self,title):
		PersistentMapping.__init__(self)
		self.title=title
		
	def values(self):
		self.expire_listings()
		return PersistentMapping.values(self)
		
	def expire_listings(self):
		"delete old job listings"
		for key,job in self.items():
			if job.expired():
				del self[key]
	
	def add_listing(self,title,description,contact,weeks,REQUEST=None):
		"add a new job listing - called by add_html"
		# figure out listing expiry time
		expires=time.time()+weeks*60*60*24*7
		# pick a unique id
		id=self.unique_key()
		# create new listing object
		listing=JobListing(id,title,description,contact,expires)
		# add new listing
		self[id]=listing
		if REQUEST is not None:
			# if this method is called by bobo,
			# return a confirmation message
			return self.confirm_html(
				message="Job listing successfully added.")
		
	def unique_key(self):
		key=int(time.time())
		while self.has_key(key):
			key=key+1
		return str(key)
	
	# default HTML method
	index_html=HTMLFile(dtml_loc+"job_board.dtml")	
	
	# add a listing HTML form
	add_html=HTMLFile(dtml_loc+"job_add.dtml")	
	
	# confirmation HTML
	confirm_html=HTMLFile(dtml_loc+"confirm.dtml")	
	
	
class JobListing(Persistent):
	"""A job listing"""
	
	def __init__(self,id,title,description,contact,expires):
		"create a new job listing"
		self.id=id
		self.title=title
		self.description=description
		self.contact=contact
		self.expires=expires
		
	def expired(self):
		"is this job listing stale?"
		return time.time() > self.expires
	
	# default HTML method
	index_html=HTMLFile(dtml_loc+"job_listing.dtml")	

job_app.py

"""JobBoard - An example Bobo application

job_app.py - The module which is published by bobo
"""

import BoboPOS
import JobBoard

# location of the object store
db_loc="D:\\Bobo\\JobBoard\\"

def create_job_board():
	"creates a new job board"
	db["python_jobs"]=JobBoard.JobBoard("Python Jobs")
	get_transaction().commit()

# define the object store
db=BoboPOS.PickleDictionary(db_loc+"job_board.db")
if not db.has_key("python_jobs"):
	# if the object store is empty, 
	# create a new job board object
	create_job_board()

# defines the job board as the 
# only object published by this
# module
bobo_application=db["python_jobs"]

# show the tracebacks for debugging
__bobo_hide_tracebacks__=None

job_board.dtml

<html>
<head><title>Job Board: <!--#var title--></title></head>
<body bgcolor="#FFFFFF">
<h1>Job Board: <!--#var title--></h1>

<h2><a href="add_html">Add a job listing</a></h2>

<!--#if values-->
<h2>Current job listings</h2>
<ul>
<!--#in values-->
<li><a href="<!--#var id-->"><!--#var title--></a>
<!--#/in-->
</ul>
<!--#else-->
<h2>There are currently no job listings.</h2>
<!--#/if-->
</body>
</html>

job_listing.dtml

<html>
<head><title>Job Listing: <!--#var title--></title></head>
<body bgcolor="#FFFFFF">
<h1>Job Listing: <!--#var title--></h1>

<table>
<tr><td>Contact:</td><td><!--#var contact--></td></tr>
<tr valign="top"><td>Job Description:</td>
<td><!--#var description fmt="multi-line"--></td></tr>
</table>
<p><a href="../index_html">Return</a> to the job board.</p>
</body>
</html>

confirm.dtml

<html>
<head><title>Confirmation</title></head>
<body bgcolor="#FFFFFF">
<h1>Confirmation</h1>

<p><b><!--#var message--></b></p>

<p><a href="index_html">Return</a> to the job board.</p>
</body>
</html>

job_add.dtml

<html>
<head>
<title>Add Job Listing: <!--#var title--></title>
</head>
<body bgcolor="#FFFFFF">
<h1>Add Job Listing: <!--#var title--></h1>

<form action="add_listing" method="post">
<table>
<tr><td>Job title</td>
<td><input type="text" name="title" size=60></td></tr>
<tr><td>Job description</td>
<td><textarea name="description:text" cols=60 rows=10>
</textarea></td></tr>
<tr><td>Contact name and email address</td>
<td><input type="text" name="contact" size=60></td></tr>
<tr><td>Job listing expires in</td>
<td><select name="weeks:int">
<option>1
<option>2
<option>3
<option>4
<option>5
<option>6
</select>
weeks
</td></tr>
<tr><td></td>
<td><input type="submit" value="Add job listing"></td></tr>
</table>
</form>

</body>
</html>


Sun, 25 Feb 2001 03:00:00 GMT  
 
 [ 1 post ] 

 Relevant Pages 

1. Bobo Documentation and Tutorial Available

2. COM documentation and tutorial available.

3. PB32 Tutorial Books/Documentation needed

4. Some inquiries about Dylan tutorials and documentation

5. Numerical Python Documentation and Tutorial

6. mod_ruby documentation/tutorial...

7. Boa Constructor: Tutorial, Documentation, or online help????

8. expect documentation/tutorial

9. I need documentation and a tutorial

10. Documentation / Manuals / Tutorials / FAQ ?

11. Updated Office Templates documentation available

12. Additional Documentation Available for CW 2.00x

 

 
Powered by phpBB® Forum Software