The PEAK Developers' Center   HelloWorld UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View
Version as of 2003-12-06 00:36:12

Clear message


By R. David Murray (rdmurray at bitdance.com) (PJE has edited Chapter 1)

There's a long tradition of teaching programmers how to use programming systems via "Hello, World!" examples. Why should PEAK be left out? I'm going to try to develop a series of stripped-to-the-essentials examples here that provide complete working PEAK programs that demonstrate some of the basic services provided by PEAK. Don't expect to find deep knowledge here, this article is intended to help you get over the first hump of using PEAK.

(Note to Windows users: if you're not using a Unix-like shell (e.g. Cygwin/Bash), you'll need to change the command lines used here. For example, peak foo bar will become something like c:\Python22\Python.exe c:\Python22\peak foo bar. None of the #! lines will work for you, either, and to set environment variables like PYTHONPATH, you'll need to use a separate SET command. However, there's nothing Unix-specific about the framework or examples, and your applications will still run on Windows. It's just that the standard Windows shell doesn't offer as many conveniences in these areas. If you have trouble running the examples on Windows, write up your question on the PEAK mailing list, and we'll try to get these examples updated with more Windows-specific command lines.)

1 Basic Configuration

1.1 Introduction

At base, a PEAK program is a python program. PEAK is a toolkit and framework, which means it provides stuff you can use in your python programs to get things done more easily and more quickly, and usually more elegantly.

So, the simplist PEAK "Hello, world!" program is also, in a trivial sense, the python "Hello, world!" program:

 
    print "Hello, World!" 
 
This, however, is very uninteresting.

One of the things PEAK provides is a framework for creating application programs that have associated configuration information that they can access easily. The goal of this first chapter will be to show how to turn the output of the simple "Hello, world!" program into a configured string. I'm going to get us there step by step, keeping the program in a runnable state at each step (except the first!)

1.2 Running helloworld under PEAK

Since we are using a configuration framework, our trivially simple python program needs to be complexified by adding a configuration file along side the python code file containing the actual print statement. That configuration file looks like this:

 
    [peak.running] 
 
    app = importString('helloworld:HelloWorld') 
 
What this is doing is defining a configuration item named peak.running.app, and setting it to the results of calling the PEAK importString function on the string helloworld:HelloWorld. The running module of PEAK provides various services for running application programs. To use the PEAK running framework, we'll use the peak command to invoke it, and pass it our helloworld configuration file. If we put the above text into a file named helloworld, we would call it using the peak command as follows:
 
    peak runIni helloworld 
 
At the moment, this will result in nothing but some error messages. To make a working application, we need to actually provide the pieces we have declared through the configuration file.

The peak runIni command expects the peak.running.app configuration variable to point to a python object that provides one of several possible Interfaces. The simplist Interface an app object can provide is just to be callable with no parameters.

So, our helloworld.py file needs to contain a python object that when called with no parameters prints "Hello, world!". That's simple enough to arrange, and hardly more complicated than the trivial python "Hello, world" example:

 
    def HelloWorld(): 
        print "Hello, world!" 
 
Our PEAK application is now runnable.

The importString function acts like a python import statement, with the : replacing the import in the from form. The importString line of the config file show above is equivalent to the following python:

 
    from helloworld import HelloWorld as app 
 
You will note that because we are using an import statement, the module we are importing from is going to have to be on the PYTHONPATH. If we've got our two files (helloworld and helloworld.py) in the current directory, we can execute this working PEAK program as follows:
 
    PYTHONPATH=. peak runIni helloworld 
 
Just as a side note, if a basic (callable) app object like this returns a value, that value has significance. If the value is an integer, it will be used as the return code from the peak command. If it is not an integer, it will be echoed to the terminal, and 1 will be used as the return code from the peak command.

1.3 Execution via Command Name

The configuration file for a complex application is central to that application. Under unix, PEAK allows configuration files to be the "executable" file of the application, the file whose name is entered at the console to invoke the program. We do this using the "magic cookie" of the unix shell:

 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HellowWorld') 
 
This uses the unix utility env to find the peak command in the current PATH, and calls it. peak then parses the file just like it does when called via peak runIni [file] from the command line.

PEAK is still looking for the modules to import in the PYTHONPATH, though, so you have to call this helloworld like this:

 
    PYTHONPATH=. ./helloworld 
 
Don't forget to chown +x helloworld first (if you're on Unix).

In a real application, you could arrange for your modules to be in a package on the system PYTHONPATH, thus enabling your command to simply be called by name.

If you can't arrange for the package to be on the system PYTHONPATH, or you don't want it to be there, you can instead write a little shell script. In this case you'd probably call the config file helloworld.ini. If helloworld.ini and helloworld.py are in the same directory as the helloworld script, the script might look like this:

 
    #!/bin/sh 
    here=`dirname $0` 
    PYTHONPATH=$here peak runIni $here/helloworld.ini 
 
Of course, in a real app you'd probably put the script in your /usr/local/bin directory or equivalent and the module files in some special directory, and replace here with the actual path to those module files.

1.4 Application Configuration

Now we'll move the string our helloworld is going to print out into the configuration file. The configration file follows ini format, which means you have "sections" in square brackets followed by lines that set values for various names within the section. PEAK doesn't place many restrictions on what sections or names can appear in an ini file, so we can pretty much choose whatever section name and variable names we want, as long as they're valid Python identifiers.

We'll be very unoriginal and call our configuration section helloworld. The text of our message will be placed in the message configuration variable:

 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HellowWorld') 
 
    [helloworld] 
 
    message = "Hello, world!" 
 
At this point you could run helloworld again, and nothing would have changed. PEAK will have loaded the new configuration information we provided, but our program doesn't yet actually use that information.

To use the configuration information we need to obtain it from the PEAK framework. At this point we are actually starting to use PEAK facilities directly in our python code, so we'll need to get them out of the PEAK package. PEAK provides a simple way to access the vast majority of the PEAK facilities through its api module:

 
    from peak.api import * 
 
This will load into the local namespace a minimal set of names through which the PEAK framework can be accessed. It uses a lazy import mechanism, so ultimately your program will only end up loading into memory those PEAK facilities it actually uses.

In addition, we can no longer use a simple, dumb executable object as our app object. In order to access the configuration information, the app object must participate in the framework.

In this case, the framework for a simple command line command such as our helloworld is provided by the AbstractCommand class of the PEAK running.commands module:

 
    class HelloWorld(commands.AbstractCommand): 
 
An Abstract class is a superclass that cannot be used by itself. To be used, it must be subclassed and certain abstract methods overridden with "real" code in the concrete subclass.

In the case of AbstractCommand, there is only one method we need to define, the _run command, which should do the actual work of our application. So, without getting into the configurability yet, our complete helloworld.py file would now look like this:

 
    from peak.api import * 
 
    class HelloWorld(commands.AbstractCommand): 
 
        def _run(self): 
            print "Hello, world!" 
 
So, again, we can run our program at this point and get the same results as before. But we still haven't used the configuration information. To do that, we use the binding module.

The function of PEAK's binding module that we are interested in here is the Obtain function. Obtain, coupled with the PropertyName function, allows us to pull values out of the configuration file:

 
    message = binding.Obtain(PropertyName('helloworld.message')) 
 
Obtain can find things using mechanisms other than PropertyName, but PropertyName is the one we are interested in here. It takes as its argument the fully qualified property name. By that I mean the name of the section the property is found in, followed by a dot, followed by the variable name. So our message property from our helloworld section becomes the PropertyName helloworld.message.

Bindings like Obtain are "descriptors", like the Python built-in type property. So, in order for a binding to be used, it must be placed in a class, and then used from an instance of the class. So, when we access the message attribute in our _run() method, the Obtain binding will look up the configuration property for us. It does this by searching the context in which the instance is located. The peak.running.commands framework will provide us with with such a context automatically, when it creates an instance of our HelloWorld class. So here's our revised program:

 
    from peak.api import * 
 
    class HelloWorld(commands.AbstractCommand): 
 
        message = binding.Obtain(PropertyName('helloworld.message')) 
 
        def _run(self): 
            print self.message 
 
With this version of helloworld.py we are finally using the configuration information provided in the helloworld file. To prove this, first run the helloworld command:
 
    PYTHONPATH=. ./helloworld 
 
Now copy helloworld to hellonewworld and edit it to look like this:
 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HelloWorld') 
 
    [helloworld] 
 
    message = "Hello, new world!" 
 
If you now run this new command:
 
    PYTHONPATH=. ./hellonewworld 
 
you'll be greated by our new message:
 
    Hello, new world! 
 
Although this is a very simple example, it should already be clear that PEAK provides an easy to use and powerful configuration mechanism. Simple use of PropertyName and values provided in config files barely even scratch the surface of what PEAK can do, but even if you used nothing else PEAK would still be useful just for making it easy to write configurable python shell scripts!

2 Command Arguments and Simple Database Access

2.1 Introduction

Now its time to go a tiny step beyond the triviality of the "Hello, world!" example. In this chapter I'll expand the example to handle saying hello to an arbitrary variety of things. Since we want to be flexible. we'll get the greeting message for each thing from a database table.

2.2 Command Arguments

To greet more than one thing, we'll need to be able to tell our command what to greet. Let's rename our command hello, and give what we want to say hello to as the command argument:

 
    hello world 
    hello fred 
    hello Klause 
 

If all we want to do is say hello in the same boring way every time, we could revise our helloworld.py file as follows:

 
    from peak.api import * 
 
    class HelloWorld(commands.AbstractCommand): 
 
        message = binding.Obtain(PropertyName('helloworld.message')) 
 
        def _run(self): 
            print self.message + self.argv[1] 
 
The corresponding hello file would look like this:
 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HelloWorld') 
 
    [helloworld] 
 
    message = "Hello, " 
 
Now we'll get something like this:
 
    % export PYTHONPAT=. 
    % ./hello world 
    Hello, world 
    % ./hello fred 
    Hello, fred 
    % ./hello, Klause 
    Hello, Klause 
 

2.3 Models

When working with stored data, PEAK uses the concept of a "Model" for the data. Models consist of Elements and Features. Elements are objects that can conceptually be stored and retreived from databases. Features are attributes of Elements. And that, of course, is a gross oversimplification, but it will do for this simple introduction.

To facilitate grabbing data from our database, we'll define a simple Model class:

 
    from peak.api import * 
 
    class Message(model.Element): 
 
        class forname(model.Attribute): 
            referencedType = model.String 
 
        class text(model.Attribute): 
            referencedType = model.String 
 
Here Message is our Element, the thing we are going to load from our database file. forname and text are attributes our Message objects will have once loaded. I think their purposes should be pretty obvious.

You probably noticed immediatly that we are defining classes inside classes here, instead of defining attributes of classes as one normally does in python. The PEAK model module uses python's metaclass facilities to set up and configure the actual classes that will be used to hold database data.

referencedType is not necessarily the type of the data that will be stored in the attribute. It is a holder for methods that the framework machinery will use in reading, writing, and otherwise manipulating the data stored in the attribute.

(For more extensive examples of using Model types, see the bulletins example in the examples directory of the PEAK CVS tree.)

In keeping with what seems to be standard practice for PEAK programmers, we'll store the above code in a file named model.py.

2.4 Data Managers

The model by itself is simply information. To use that information we need a Data Manager. Data Managers are responsible for loading and storing the data described by the Model classes.

For the present example we're only interested in loading data from a database table. So we'll subclass QueryDM, a PEAK storage class that provides a read-only interface to a datastore:

 
    from peak.api import * 
 
    class MessageDM(storage.QueryDM): 
 
This class isn't useful until it is specialized. The specialization takes care of the actual reading and writing of data from the Model attributes to and from the external datastore.

To keep our example simple, we'll use a flat file as our external data store. In keeping with PEAK design principles, we won't hardcode the file into our Data Manager, but will instead make it configurable:

 
        file = binding.Obtain(PropertyName('helloworld.messagefile')) 
 
Obviously, we'll now need a different line in our hello configuration file::
 
    [helloworld] 
 
    messagefile = config.fileNearModule('helloworld', 'hello.list') 
 
Here we've introduced a new PEAK service. configs fileNearModule will construct an appropriately qualified filename for the second argument based on the assumption that it is in the same directory as the first argument.

Since we're only using a QueryDM in this example, we only have to worry about reading data from the datastore, not writing it (That's a later example). To specify how to do this we override the _load method of QueryDM. Our _load method needs to return a dictionary of names and values, which will get used through a __setstate__ call to load the data into our python class.

Now we need to whip up a data format for our messagefile. Let's have the thing we are saying hello to be first, and the actual message second, separated by a "|" character.

So we'll create a hello.list file like this:

 
    world  | Hello, world! 
    Fred   | Greatings, good sir. 
    Klause | Guten Aben, Herr Klause. 
 
(Forgive my feeble attempts at Deutsche.)

Here's the complete contents of the last new file we need for our expanded hello application, the storage.py file. This adds the _load method to the QueryDM:

 
    from peak.api import * 
    from model import Message 
 
    class MessageDM(storage.QueryDM): 
 
        defaultClass = Message 
        file = binding.Obtain(PropertyName('helloworld.messagefile')) 
        data = {} 
 
        def _load(self, oid, ob): 
            if self.data: return self.data[oid] 
            f = open(self.file) 
            for l in f: 
                parts = l.split('|') 
                for i in range(len(parts)): parts[i] = parts[i].strip() 
                data[parts[0]] = {'forname': parts[0], 'text': parts[1]} 
            f.close() 
            return data[oid] 
 
defaultClass points to the class that will be used to instantiate objects retreieved from this Data Manager. In our case that's Message from our model class. binding.Obtain you've met before, so its purpose should be obvious.

_load is going to get called when our application program gets as far as trying to access an attribute of an object returned from the Data Manager under some key. When that happens, _load gets passed the oid that was used to access the object originally, and the current ghost, or possibly partially loaded, object. Right now don't worry about those possabilities. What _load returns will be used to load the object for real, after which the attribute access that triggered the _load call can finally be satisfied.

Because we're using a simple file, and we're only reading, we can do some things in our _load method that we wouldn't want to do for a writable database. Specifically, I've rigged things so that on the first load attempt we'll read in the datastore file, parse it, and cache the data in the data attribute of the Data Manager. After that requests to load objects will be satisfied from that cached data.

Of course, in the case of our current hello program, we'll only ever make one query on the database. But by following PEAK design principles, we've already future proofed our program against the possability we might want be making more than one greeting in a single hello call.

One final thing to point out about this class: we are using the forname as the key to look up records in our database. Thus it is the oid (Object Id) used by the Data Manager to refer to the Message instance that will hold the data from the corresponding record.

2.5 Putting it all Together

All that remains is to use this database from our main application program:

 
    from peak.api import * 
    from model import Message 
 
    class HelloWorld(commands.AbstractCommand): 
 
        Messages = binding.Make( 
            'storage:MessageDM', 
            offerAs=[storage.DMFor(Message)] 
            ) 
 
        def _run(self): 
            storage.beginTransaction(self) 
            print self.Messages[self.argv[1]].text 
            storage.commitTransaction(self) 
 
As you can see, our main program has stayed fairly simple, despite the considerable complexity provided by using a database. And in case you think "using a database" is an inflated way of refering to a flat file, observe that we can replace the simple flat file with access to something like an sql database simply by changing the _load method in our storage.py file.

The binding.Make directive is another example of using the binding module to create a configuration based descriptor in a class. In this case we're creating the value that will be provided, and then offering it to the configuration system as a storage.DMFor(Message), which means as a Data Manager for the Message Element class. The storage:MessageDM argument has the same syntax and semantics as the string passed to the importString function discussed in the first chapter. In this case the binding system (gets ready to) instantiate an instance of the MessageDM class we defined in our storage.py file. We also assign the MessageDM reference thus created to the Messages class variable, which is where we're going to actually use it from. But once we've offered it in this way it could also be referneced through the binding system elsewhere in our application simply by saying:

 
    someclassvar = binding.Obtain(storage.DMFor(Message)) 
 

The new version of the print statement should be pretty understandable. Once we've got the Message Data Manager instance, we can look up any Message instance in it by using the instance's oid as a key. In our case, that's the 'forname' string passed in as an argument to our command. The object thus retreived has the attributes defined in our model; in this case we are using the text attribute.

The last new bit is the xxxTransaction calls. Database accesses must take place inside transactions. For our simple read-only application here this doesn't really matter, but it will matter a great deal when you use read/write databases.

At this point, our program works like this:

 
    % export PYTHONPAT=. 
    % ./hello world 
    > Hello, world! 
    % ./hello Fred 
    > Greetings, good sir. 
    % ./hello Klause 
    > Guten Aben, Herr Klause. 
 

For another example of using the binding and storage frameworks, check out PeakDatabaseApplications.


PythonPowered
EditText of this page (last modified 2003-12-06 00:36:12)
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, DeletePage, LikePages, LocalSiteMap, SpellCheck