The PEAK Developers' Center   Diff for "HelloWorld" UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View
Ignore changes in the amount of whitespace

Differences between version dated 2003-12-06 15:18:58 and 2003-12-09 22:22:19 (spanning 39 versions)

Deletions are marked like this.
Additions are marked like this.

#DEPRECATED
 
Please use IntroToPeak instead.
 
By R. David Murray (rdmurray at bitdance.com) (w/edits by PJE)
 
'''Preface'''

write up your question on the PEAK mailing list, and we'll try to
get these examples updated with more Windows-specific command lines.)
 
(Note to all readers: the examples in this document have been tested
against PEAK CVS. The examples will not work with 0.5 alpha 2.
There could also be synchronization errors between the examples and
the test code, so if you find something that doesn't work as
advertised when run against current CVS, please let me know.)
 
'''Table of Contents'''
 
[[TableOfContents]]

So, the simplest PEAK "Hello, world!" program is also, in a trivial
sense, the Python "Hello, world!" program: {{{
#!python
    print "Hello, World!"
print "Hello, World!"
 
}}} This, however, is very uninteresting.
}}} This, however, is not very interesting.
 
One of the things PEAK provides is a framework for giving application
programs easy access to their associated configuration data. The goal

Okay, that looks helpful, although the stuff about command interfaces
and shortcuts is perhaps a little intimidating. For now, we'll stick to
the easy part, where it said we could use a callable! Let's make a
`helloworld` module, with a `HelloWorld` function: {{{
`helloworld` module, with a `HelloWorld` function, in `helloworld.py`: {{{
#!python
    def HelloWorld():
        print "Hello, world!"
def HelloWorld():
    print "Hello, world!"
 
}}}
 

Traceback (most recent call last):
  File "C:\Python22\Scripts\peak", line 7, in ?
    sys.exit(
  File "C:\cygwin\home\pje\PEAK\src\peak\running\commands.py", line 303, in __ca
ll__
  File "C:\cygwin\home\pje\PEAK\src\peak\running\commands.py", line 303, in __call__
    return cmd.interpret(cmd.argv[1])
  File "C:\cygwin\home\pje\PEAK\src\peak\running\commands.py", line 624, in inte
rpret
  File "C:\cygwin\home\pje\PEAK\src\peak\running\commands.py", line 624, in interpret
    raise InvocationError("Name not found: %s" % name)
peak.running.commands.InvocationError: Name not found: import:helloworld:HelloWo
rld
peak.running.commands.InvocationError: Name not found: import:helloworld:HelloWorld
 
}}}
 

% export PYTHONPATH=.
% peak import:helloworld.HelloWorld
Hello, world!
 
}}}
 
Nice! But we said we were going to make the message configurable.
To do that, we're going to use the `peak runIni` command.
 
== The "runIni" Command ==
== ".ini" Files and the "runIni" Command ==
 
`runIni` is one of the built-in "shortcut" commands that were alluded to in
the `peak` script's usage message. It takes the argument

property. But it doesn't have to actually be named with `.ini`, so let's
just call our configuration file `helloworld`: {{{
 
    [peak.running]
    app = importString("helloworld.HelloWorld")
[peak.running]
app = importString("helloworld.HelloWorld")
 
}}}
 
How did we know to format it this way? Well, we looked at the `peak.ini`
(Note to experienced Pythonistas: PEAK does ''not'' use the Python
`ConfigParser` module to parse `.ini` files, so don't assume that
you can use `ConfigParser` syntax or semantics here. To meet PEAK's
configuration requirements, both the syntax and semantics needed to
be different from those supplied by `ConfigParser`.)
 
How did we know to format the file this way? Well, we looked at the `peak.ini`
file, of course, and learned by example. Here we've defined a configuration
rule for the property named `peak.running.app`. The rule says (in effect),
"when someone asks for the `peak.running.app` property, call the

 
}}}
 
The reason we say "roughly" is that PEAK configuration files aren't executed
the way Python code is. Properties in the configuration file are not Python
assignment statements: they're ''rules'' that supply a Python expression
whose value is computed only when the property is looked up. When the
configuration file is loaded, the expressions are read, but not executed.
Indeed, if the property is never looked up, the expression is never
evaluated at all.
 
This can be a big advantage over using a Python script for configuration,
since you can create a shared .ini file that describes
properties used by several applications. Even if some of those properties
might involve a time-consuming operation such as opening a database
connection, only the applications that actually use the property will
pay the cost of computing it.
 
Indeed, this allows you to use a single site-wide configuration file to
provide configuration for all of your PEAK-based applications, using the
PEAK_CONFIG environment variable to point to the file. Of course, the
applications can't use property names that conflict with other
applications' property names. But, there are many configuration
properties used by PEAK that you may wish to share across multiple
applications, such as logging rules, naming services, and command
shortcuts, to name just a few. Property definitions supplied by
application-specific configuration files will of course override any
definitions in the site-wide configuration defaults.
 
Anyway, we now have a configuration file for our application, trivial
as it is so far. Let's try it out: {{{
 
% peak runIni helloworld
Hello, world!
 
}}}
 
It still works.
Looks like it still works.
 
By the way, 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 (or `None`), it will be used as the return code from the `peak`
script, and of our command line as a whole. 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` script. If you want, try this out now by adding
various `return` statements to your `helloworld.py` and checking the
exit code from the `peak` script in your shell.
By the way, just as a side note, if an application object like our
`HelloWorld` function returns a value, that value has significance.
If the value is an integer (or `None`), it will be used as the return
code from the `peak` script, and of our command line as a whole. 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` script. If you want, try
this out now by adding various `return` statements to your
`helloworld.py` and checking the exit code from the `peak` script in
your shell.
 
 
== Executable Configuration Files ==
 
The configuration file for a complex application is central to
The configuration file for a complex application is often central to
that application. Under Unix-like command shells, 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. This is done using the "magic cookie" of the unix shell: {{{
 
    #!/usr/bin/env peak runIni
 
    [peak.running]
#!/usr/bin/env peak runIni
 
    app = importString('helloworld:HellowWorld')
[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

using a Unix-like OS, as opposed to just a Unix-like shell.)
 
Python still needs imported modules to be on the
`PYTHONPATH`, though, so you have to call this helloworld
like this: {{{
`PYTHONPATH`, though, so you have to call this `helloworld`
file like this: {{{
 
    PYTHONPATH=. ./helloworld
% PYTHONPATH=. ./helloworld
 
}}} or else set the `PYTHONPATH` separately, as we did earlier. In
that case, you can just run `./helloworld`. Try it now.
}}} (Or else set the `PYTHONPATH` separately, as we did earlier. In
that case, you can just run `./helloworld`. Try it now.)
 
In a real application, you could of course install your modules
in a directory on the system `PYTHONPATH` (such as in the Python

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
#!/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

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.
place many restrictions on what sections or property names can appear in an
`.ini` file, so we can pretty much choose whatever section name and
property 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: {{{
`helloworld`. (It's generally a good idea to have an application's
properties grouped under names that begin with the application name,
so they won't conflict with those of other applications.) The text
of our message will be placed in the `message`
configuration property: {{{
 
    #!/usr/bin/env peak runIni
#!/usr/bin/env peak runIni
 
    [peak.running]
[peak.running]
app = importString('helloworld:HellowWorld')
 
    app = importString('helloworld:HellowWorld')
[helloworld]
message = "Hello, world!"
 
    [helloworld]
 
    message = "Hello, world!"
 
}}} At this point you could run helloworld again, and nothing would
}}} At this point, you can run `helloworld` again, and nothing will
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 via the
PEAK configuration 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
import them from 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 *
from peak.api import *
 
}}} This will load into the local namespace a minimal set of names
through which the PEAK frameworks can be accessed. It uses a lazy
through which all of the PEAK frameworks 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.
into memory those PEAK facilities it actually uses. (As you might be
noticing, "lazy loading" and "lazy evaluation" are recurring themes
in PEAK. We'll be running into them over and over again, in this
and subsequent chapters.)
 
In addition, we can no longer use a simple, dumb function object
as our application. In order to access the configuration information,

as our `helloworld` is provided by the `AbstractCommand` class of the
`peak.running.commands` module (lazily imported for us by `peak.api`): {{{
 
    class HelloWorld(commands.AbstractCommand):
class HelloWorld(commands.AbstractCommand):
 
}}} An "abstract" class is a base class that cannot be used by itself.
To be used, it must be subclassed and certain abstract methods

 
In the case of `AbstractCommand`, there is only one method we need
to override: the `_run` method, which will do the actual work of
our application. So, without getting into the configurability yet,
our application (and optionally return an exit code, just like our
function could). So, without getting into the configurability yet,
our complete `helloworld.py` file would now look like this: {{{
#!python
    from peak.api import *
from peak.api import *
 
class HelloWorld(commands.AbstractCommand):
 
    def _run(self):
        print >>self.stdout, "Hello, world!"
 
    class HelloWorld(commands.AbstractCommand):
}}} Hmm, what's that output redirection? Doesn't print normally
write to `stdout`?
 
        def _run(self):
            print "Hello, world!"
Indeed, it does. And `stdout` is normally the shell command's
`stdout`, and certainly is in our little application here. But an
`AbstractCommand` can be run in many environments, not just from a
shell script the way we are doing here. So to future-proof our
code, we use the facilities `AbstractCommand` provides for communicating
with its environment, whatever that may turn out to be.
 
}}} So, again, we can run our program at this point and get the same
(In addition to `self.stdout`, `AbstractCommand` also provides
`self.stdin`, `self.stderr`, `self.environ`, and `self.argv`.)
 
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 need the `binding` package, to "bind"
an attribute of our application to a configuration variable.

property `helloworld.message`. Instead it means, "when the `message`
attribute of an instance of this class is accessed, obtain the
`helloworld.message` property, and use that as the attribute's value
from then on."
from then on." (See? We told you lazy loading was a recurring theme
in PEAK!)
 
In other words, 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.
Notice that this means bindings like `Obtain` are Python "descriptors",
similar to 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.
 
`Obtain` can find things using mechanisms other than `PropertyName`,
but `PropertyName` is the one we are interested in here. `PropertyName`
is a string subclass, and its constructor takes as its argument the fully
qualified property name. By "fully qualified", I mean
the name of the section the property is found in, followed by a
dot, followed by the variable name. So the `message` property from
our `helloworld` section becomes the `PropertyName` `helloworld.message`.
dot, followed by the rest of the property name. So the `message`
property from our `helloworld` section becomes the `PropertyName`
`helloworld.message`.
 
The `PropertyName` class ensures that its instances are of
valid syntax for use in a configuration file, but that's not the only

with with such a context automatically, when it creates an instance
of our `HelloWorld` class. (We won't deal with the specifics of
this mechanism here; suffice to say that some code we inherit from
`AbstractCommand` will take care of the details.)
`AbstractCommand` will take care of the details for us.)
 
Let's look at the revised program: {{{
#!python
    from peak.api import *
from peak.api import *
 
    class HelloWorld(commands.AbstractCommand):
class HelloWorld(commands.AbstractCommand):
 
        message = binding.Obtain(PropertyName('helloworld.message'))
    message = binding.Obtain(PropertyName('helloworld.message'))
 
        def _run(self):
            print self.message
    def _run(self):
        print >>self.stdout, 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: {{{
 
    % ./helloworld
    Hello, world!
% ./helloworld
Hello, world!
 
}}} Now copy `helloworld` to `hellonewworld` and edit it to look like this: {{{
 
    #!/usr/bin/env peak runIni
 
    [peak.running]
#!/usr/bin/env peak runIni
 
    app = importString('helloworld.HelloWorld')
[peak.running]
app = importString('helloworld.HelloWorld')
 
    [helloworld]
 
    message = "Hello, new world!"
[helloworld]
message = "Hello, new world!"
 
}}} If you now run this new command: {{{
 
    % ./hellonewworld
% ./hellonewworld
 
}}} you'll be greeted by our new message: {{{
 
    Hello, new world!
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

 
  * The return value of an executed function (or `_run()` method) will become the exit code for the entire program, when it is run by the `peak` script.
 
  * `_run` implementations should use the variables provided in `self` to communicate with their environment.
 
 * Configuration files
 
  * PEAK's configuration framework supports `.ini`-style configuration files, using property names as section headings, and Python expressions for supplied values. (Note: PEAK also supports other configuration formats, but the `.ini` format is most useful for configuration done by developers. Other formats (such as ZConfig) may be more suitable for end-users.)
 
  * The `PEAK_CONFIG` environment variable can name a configuration file that will be used to supply configuration defaults, before application-specific configuration is loaded.
 
  * Properties are defined in `.ini` files with Python expressions that are "lazily evaluated" when the property is looked up. Simply loading the file does not execute the expressions.
 
  * The `peak runIni` command can "execute" a configuration file, by looking for its `peak.running.app` property. The property can be any "executable" just as could be run directly by the `peak` script.
 
  * With Unix-like shells, a configuration file can be made executable, by using `peak runIni` as part of the file's `#!` "magic cookie" line.

command what to greet. Let's rename our command to `hello`, and give
what we want to say hello to as the command argument, e.g.: {{{
 
    hello world
    hello fred
    hello Klause
% 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: {{{
#!python
    from peak.api import *
from peak.api import *
 
    class HelloWorld(commands.AbstractCommand):
class HelloWorld(commands.AbstractCommand):
 
        message = binding.Obtain(PropertyName('helloworld.message'))
 
        def _run(self):
            print self.message % self.argv[1]
    message = binding.Obtain(PropertyName('helloworld.message'))
 
}}} The corresponding `hello` file would look like this: {{{
    def _run(self):
        print >>self.stdout, self.message % self.argv[1]
 
    #!/usr/bin/env peak runIni
}}} Note the use of `self.argv` to get access to the command
arguments.
 
    [peak.running]
The corresponding `hello` file would look like this: {{{
 
    app = importString('helloworld.HelloWorld')
#!/usr/bin/env peak runIni
 
    [helloworld]
[peak.running]
app = importString('helloworld.HelloWorld')
 
    message = "Hello, %s. How are you today?"
[helloworld]
message = "Hello, %s. How are you today?"
 
}}} Now we'll get something like this: {{{
 
    % export PYTHONPATH=.
    % ./hello world
    Hello, world. How are you today?
    % ./hello fred
    Hello, fred. How are you today?
    % ./hello, Klause
    Hello, Klause. How are you today?
% export PYTHONPATH=.
% ./hello world
Hello, world. How are you today?
% ./hello fred
Hello, fred. How are you today?
% ./hello Klause
Hello, Klause. How are you today?
 
}}}
 

To facilitate grabbing data from our database, we'll define a simple
Element class, in a `hello_model.py` file: {{{
#!python
    from peak.api import *
from peak.api import *
 
    class Message(model.Element):
class Message(model.Element):
 
        class forname(model.Attribute):
            referencedType = model.String
    class forname(model.Attribute):
        referencedType = model.String
 
        class text(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

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 *
from peak.api import *
 
    class MessageDM(storage.QueryDM):
class MessageDM(storage.QueryDM):
 
}}} This class is another abstract class that has to be specialized
for our intended use. Specifically, we have to add the code that

hardcode the filename into our Data Manager, but will instead make it
configurable: {{{
 
        filename = binding.Obtain(PropertyName('helloworld.messagefile'))
    filename = 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')
[helloworld]
messagefile = config.fileNearModule('helloworld', 'hello.list')
 
}}} Here we've used another PEAK function: `config.fileNearModule()`
will construct an appropriately qualified filename for the second

 
So we'll create a `hello.list` file like this: {{{
 
    world | Hello, world!
    Fred | Greetings, good sir.
    Klause | Guten Aben, Herr Klause.
world | Hello, world!
Fred | Greetings, good sir.
Klause | Guten Aben, Herr Klause.
 
}}} (Forgive my feeble attempts at Deutsche.)
 

load the file only once, the first time it's used. We'll use another
`peak.binding` tool to accomplish this: {{{
#!python
        def data(self):
            data = {}
            file = open(self.filename)
            for line in file:
                fields = [field.strip() for field in line.split('|',1)]
                forname, text = fields
                data[forname] = {'forname': forname, 'text': text}
            file.close()
            return data
    def data(self):
        data = {}
        file = open(self.filename)
        for line in file:
            fields = [field.strip() for field in line.split('|',1)]
            forname, text = fields
            data[forname] = {'forname': forname, 'text': text}
        file.close()
        return data
 
        data = binding.Make(data)
    data = binding.Make(data)
}}}
 
`binding.Make` is similar to `binding.Obtain`, in that it's used inside

expanded `hello` application, the `hello_storage.py` file. This also adds the
`_load` method to the `QueryDM`: {{{
#!python
    from peak.api import *
    from hello_model import Message
from peak.api import *
from hello_model import Message
 
    class MessageDM(storage.QueryDM):
class MessageDM(storage.QueryDM):
 
        defaultClass = Message
        filename = binding.Obtain(PropertyName('helloworld.messagefile'))
    defaultClass = Message
    filename = binding.Obtain(PropertyName('helloworld.messagefile'))
 
        def data(self):
            data = {}
            file = open(self.filename)
            for line in file:
                fields = [field.strip() for field in line.split('|',1)]
                forname, text = fields
                data[forname] = {'forname': forname, 'text': text}
            file.close()
            return data
    def data(self):
        data = {}
        file = open(self.filename)
        for line in file:
            fields = [field.strip() for field in line.split('|',1)]
            forname, text = fields
            data[forname] = {'forname': forname, 'text': text}
        file.close()
        return data
 
        data = binding.Make(data)
    data = binding.Make(data)
 
        def _load(self, oid, ob):
            return self.data[oid]
    def _load(self, oid, ob):
        return self.data[oid]
 
}}} `defaultClass` specifies the class that will be used to instantiate
objects retreieved from this Data Manager. In our case, that's

kind. So, when we use a `MessageDM` instance, we'll retrieve objects
from it like this: {{{
 
    aMessage = myMessageDM[someName]
        aMessage = myMessageDM[someName]
 
}}}
 

All that remains, then, is to use our new data manager from our main application
program: {{{
#!python
    from peak.api import *
    from model import Message
from peak.api import *
from hello_model import Message
 
    class HelloWorld(commands.AbstractCommand):
class HelloWorld(commands.AbstractCommand):
 
        Messages = binding.Make(
            'hello_storage.MessageDM',
            offerAs=[storage.DMFor(Message)]
        )
 
        def _run(self):
            storage.beginTransaction(self)
            print self.Messages[self.argv[1]].text
            storage.commitTransaction(self)
    Messages = binding.Make(
        'hello_storage.MessageDM',
        offerAs=[storage.DMFor(Message)]
    )
 
    def _run(self):
        storage.beginTransaction(self)
        print >>self.stdout, self.Messages[self.argv[1]].text
        storage.commitTransaction(self)
 
}}} As you can see, our main program has stayed fairly simple,
despite the additional complexity of using a database.

 
Anyway, our program now works like this: {{{
 
    % export PYTHONPATH=.
    % ./hello world
    > Hello, world!
    % ./hello Fred
    > Greetings, good sir.
    % ./hello Klause
    > Guten Aben, Herr Klause.
% export PYTHONPATH=.
% ./hello world
Hello, world!
% ./hello Fred
Greetings, good sir.
% ./hello Klause
Guten Aben, Herr Klause.
 
}}}
 

 
Whew! That's a lot of things, but we're still only scratching the surface. Each of the major topic areas listed above could be expanded into entire tutorials of their own. But, for now, you may want to simply experiment a bit with what you've seen so far, before delving deeper into the PEAK API documentation and source code.
 
== Exploring the rest of PEAK ==
 
= Multiple Commands and Writable Databases =
 
(Note: This chapter has not yet been edited by PJE)
 
We're stretching pretty hard here to find ways to use "Hello, world!"
to demonstrate PEAK concepts. Just keep supressing your impulse
to laugh at the triviality of the example, and keep focused on the
concepts it's demonstrating.
 
At this point our `hello` program can greet anything whose name
is recorded in the table. Suppose we have a new thing we want
to greet? What we need is a way to update the database table
to record a new greeting. Our `hello` program will now have two
different functions we need it to perform: greeting, and recording
new greetings. (Of course, we could also implement these as
two separate commands, but then we wouldn't have an excuse to talk
talk about `AbstractInterpreter` and demonstrate `Bootstrap`).
 
So in this chapter we'll expand the `hello` command to have
subcommands: {{{
 
    % ./hello for Jeff: Hi, guy!
    % ./hello to Jeff
    Hi, guy!
 
}}} This will require revising our storage implementation to allow
writing to our database. We'll stick to using a file for now, to
keep the distraction of SQL at bay for a little while longer.
 
== AbstractInterpreter and Bootstrap ==
 
Another abstract class provided by PEAK is `AbstractInterpreter`.
This class represents something PEAK can run that will execute
subcommands based on the first argument word. PEAK also provides
an subclass of `AbstractInterpreter` called `Bootstrap`, that looks
up a URL or command shortcut and runs it.
 
If you think that sounds a lot like what the `peak` script does, you're
quite correct. If you take a look at the actual `peak` script, it looks
something like: {{{
#!/usr/bin/env python2.2
 
from peak.running import commands
commands.runMain( commands.Bootstrap )
}}}
 
Which means that we've actually been using `Bootstrap` all along, to
run our programs. Since we'd like to be able to use commands like
`hello to` and `hello for` in the same way that we can use `peak help`
or `peak runIni`, we'll make our new main program a subclass of
`commands.Bootstrap`: {{{
#!python
from peak.api import *
from hello_model import Message
 
 
class HelloWorld(commands.Bootstrap):
 
    usage = """
Usage: hello command arguments
 
Available commands:
 
    for -- sets a greeting
    to -- displays a greeting
"""
 
    Messages = bindings.Make(
        'hello_storage.MessageDM', offerAs=[storage.DMFor(Message)]
    )
 
 
class toCmd(commands.AbstractCommand):
 
    usage = """
Usage: hello to <name>
 
Displays the greeting for "name".
"""
 
    Messages = binding.Obtain(storage.DMFor(Message))
 
    def _run(self):
        storage.beginTransaction(self)
        print >>self.stdout, self.Messages[self.argv[1]].text
        storage.commitTransaction(self)
 
}}} So we've got a new `HelloWorld` main program, this time a
subclass of `commands.Bootstrap`. `HelloWorld` is still the holder
for the `MessageDM` binding. The `Bootstrap` class will automatically
make the subcommands into children of our `HelloWorld` component, so
the subcommands will be able to `Obtain` the DM from their context.
 
The only other thing it's got is a `usage` class variable.
To see how this is used, try typing `./hello` at your
command prompt: {{{
 
% ./hello
 
Usage: hello command arguments
 
Available commands:
 
    for -- sets a greeting
    to -- displays a greeting
 
 
./hello: missing argument(s)
 
}}} As you can see, PEAK is taking care of a lot of the routine
tasks associated with writing a script!
 
Our original `AbstractCommand` is still there, but now we've named
it `toCmd`. And, since it a different class from `HelloWorld` where
we defined the binding to our `MessageDM`, it needs to `Obtain`
that binding. Remember, it is thereby getting access to the
'''same''' `MessageDM` instance as the one in the associated
`HelloWorld` instance. So, by using `Make(...,offerAs=[something])`
in a parent component, and `Obtain(something)` in child components,
a parent component can share one instance of a service with any
child component that needs it.
 
== peak.running.shortcuts ==
 
Next, we need to hook up the `toCmd` class so it can be invoked as
a subcommand. How can we do that? Remember when we looked at the
help for the `peak` script? The first paragraph said: {{{
 
The 'peak' script bootstraps and runs a specified command object or command
class. The NAME_OR_URL argument may be a shortcut name defined in the
'peak.running.shortcuts' property namespace, or a URL of a type
supported by 'peak.naming'.
 
}}}
 
Hm. If we look at the `peak.ini` file, we see a section called
`[peak.running.shortcuts]`, containing a bunch of properties called
`runIni`, `help`, and many other commands.
 
Does this mean that if we add a similar section to our `hello` file,
we can create subcommands of our own? Let's try adding this new
section to `hello`: {{{
 
[peak.running.shortcuts]
to = importString('helloworld.toCmd')
 
}}}
 
and now try it: {{{
 
    % ./hello to Fred
    Greetings, good sir.
 
}}}
 
Excellent! By the way, you may have noticed that when we
turned our command into a subcommand, we did ''not'' need to change
our `argv` index number. The argument array stored in `self.argv`
of the subcommand has the subcommand name in `argv[0]`, and the rest
of the arguments starting in `argv[1]`. That's because
`AbstractInterpreter` classes like `Bootstrap` automatically shift
the arguments over for us when they create the subcommand object.
 
Also by the way, we should mention that it wasn't strictly necessary
to edit the configuration file to do what we just did. We also could
have defined a binding in our `HelloWorld` class to "offer" the right
configuration value, like this: {{{
 
    __toCmd = binding.Obtain(
        'import:helloworld.toCmd',
        offerAs=['peak.running.shortcuts.to']
    )
 
}}}
 
But now that you've seen how, you can also see why we didn't do it.
It's rather ugly to do this sort of configuration in code, compared
to using an `.ini` file. But it's nice to know you can do it if you
need to.
 
Of course, the configuration file is also more flexible: notice, for
example, that we could make multiple configuration files for the same
code, each file specifying a different set of subcommands, perhaps
for different users of the app. You could almost say that PEAK's
motto is, "code reuse, through flexibility".
 
Now for something completely different. Let's try this: {{{
 
    % ./hello to
 
}}} Given how `./hello` magically generated a usage string, you
might think this would do so as well. After all, we provided one
in the code above, right? Well, an `AbstractCommand` doesn't
automacially display a usage when no arguments are supplied because,
after all, no arguments might be required. It '''will''' automatically
display the usage if we raise a `commands.InvocationError`, in our
`_run` method, though:
{{{
    def _run(self):
        if len(self.argv)<2:
            raise commands.InvocationError("Missing name")
        storage.beginTransaction(self)
        print >>self.stdout, self.Messages[self.argv[1]].text
        storage.commitTransaction(self)
}}}
 
Now we'll get: {{{
 
%s ./hello to
 
Usage: hello to <name>
 
Displays the greeting for "name".
 
to: Missing name
 
}}}
 
There's just one problem left with the `hello` command. Try running
`hello runIni`, and see what we get: {{{
 
% ./hello runIni
 
Usage: peak runIni CONFIG_FILE arguments...
 
CONFIG_FILE should be a file in the format used by 'peak.ini'. (Note that
it does not have to be named with an '.ini' extension.) The file should
define a 'running.IExecutable' for the value of its 'peak.running.app'
property. The specified 'IExecutable' will then be run with the remaining
command-line arguments.
 
 
runIni: missing argument(s)
 
}}}
 
Whoops! Just because our configuration file contains its own
`[peak.running.commands]` section, doesn't mean that the settings in
`peak.ini` don't apply. We need to do something about this, so that
`hello` doesn't reuse all the `peak` subcommands.
 
Looking at `peak.ini`, we sometimes see that properties sometimes
end with a `*`. What happens if we define a `*` rule in the shortcuts
section,? {{{
 
[peak.running.shortcuts]
* = commands.NoSuchSubcommand
to = importString('helloworld.toCmd')
 
}}}
 
Let's try it now: {{{
 
% ./hello runIni
 
Usage: hello command arguments
Available commands:
 
    for -- sets a greeting
    to -- displays a greeting
 
 
runIni: No such subcommand 'runIni'
 
}}}
 
Good. To recap, we used `commands.NoSuchSubcommand`, which
raises an `InvocationError` for us, and we used a `*` rule to
define a default value for properties whose names are within a
particular "property namespace". That is, any name we look up
in `peak.running.shortcuts` from our configuration file, that
isn't explicitly defined there or in our app, will return the
`commands.NoSuchSubcommand` class. That's just what we want
for now.
 
Actually... there is still one more problem. `commands.Bootstrap`
also accepts URLs on the command line by default. `commands.Bootstrap`
provides a way to turn that behvior off, though. We just need to
add a flag variable to our `HelloWorld` class: {{{
#!python
class HelloWorld(commands.Bootstrap):
 
    acceptURLs = False
}}}
 
With these changes, our Bootstrap derivative will now do the right
thing. Let's move on to the `for` command now.
 
== Storing a New Message: the "for" Subcommand ==
 
Now, we know we're going to have to rewrite our `hello_storage.py` to
allow us to write to the databse, but let's start this part of the
task by writing the subcommand first. As you'll quickly see, any
consideration of how we implement the saving of the data is
completely independent of how we go about initiating the save
in the application program.
 
So, we need another
rule in our `hello` configuration file: {{{
 
[peak.running.shortcuts]
* = commands.NoSuchSubcommand
to = importString('helloworld.toCmd')
for = importString('helloworld.forCmd')
 
}}} and another `AbstractCommand` subclass in `helloworld.py`
 
{{{
 
class forCmd(commands.AbstractCommand):
 
    usage = """
Usage: hello for <name>: <greeting>
 
Stores "greeting" as the greeting message for "name".
"""
 
    Messages = binding.Obtain(storage.DMFor(Message))
 
    def _run(self):
        if len(self.argv)<2:
            raise commands.InvocationError("Missing arguments")
        parts = ' '.join(self.argv[1:]).split(':',1)
        if len(parts)!=2:
            raise commands.InvocationError("Bad argument format")
        forname, message = parts
        storage.beginTransaction(self)
        newmsg = self.Messages.newItem()
        newmsg.forname = forname.strip()
        newmsg.text = message.strip()
        storage.commitTransaction(self)
 
}}} To put a new object in our database, we ask the Data Manager for
a new "blank" object. (Actually, it can have a preloaded default
state, but we'll ignore that for now). Then we modify it just like
we would any other writable object we got from the Data Manager,
and the transaction machinery takes care of getting the data written
to the backing store at transaction commit time.
 
At this point the `for` subcommand of our `hello` command is
runable: {{{
 
% ./hello for
 
Usage: hello for <name>: <greeting>
 
Stores "greeting" as the greeting message for "name".
 
for: Missing arguments
 
% ./hello for foobar
 
Usage: hello for <name>: <greeting>
 
Stores "greeting" as the greeting message for "name".
 
for: Bad argument format
 
% ./hello for Jeff: Hi, guy!
Traceback (most recent call last):
  File "/usr/local/bin/peak", line 4, in ?
    commands.runMain( commands.Bootstrap )
  File "/usr/local/lib/python2.3/site-packages/peak/running/commands.py", line 70, in runMain
    result = factory().run()
  File "/usr/local/lib/python2.3/site-packages/peak/running/commands.py", line 211, in run
    return self._run() or 0
  File "/var/home/rdmurray/proj/peak/helloworld/07writabledb/helloworld.py", line 53, in _run
    newmsg = self.Messages.newItem()
AttributeError: 'MessageDM' object has no attribute 'newItem'
 
}}} Ah, yes. As you'll recall, we used a read-only Data Manager base
class when we developed our database. So we can't store anything
until we fix that.
 
 
== Storing a New Message: Modifying the Data Manager ==
 
OK, it's time to do some serious surgery on our Data Manager. As
I said before, I'm still going to keep us away from SQL for the
moment. But if you'll recall, I mentioned how important transactions
are when writing to a database. Writing to normal file system files
is '''not''' transaction oriented, so what can we do?
 
PEAK provides a useful utility class that partially solves this
problem: `EditableFile`. `EditableFile` is an object that represents
a file and participates in the PEAK transaction machinery.
 
Here's what the `EditableFile` docstring has to say: {{{
    """File whose text can be manipulated, transactionally
 
    Example::
 
        myfile = EditableFile(self, filename="something")
        print myfile.text # prints current contents of file
 
        # Edit the file
        storage.beginTransaction(self)
        myfile.text = myfile.text.replace('foo','bar')
        storage.commitTransaction(self)
 
    Values assigned to 'text' will be converted to strings. Setting 'text'
    to an empty string truncates the file; deleting 'text' (i.e.
    'del myfile.text') deletes the file. 'text' will be 'None' whenever the
    file is nonexistent, but do not set it to 'None' unless you want to replace
    the file's contents with the string '"None"'!
 
    By default, files are read and written in "text" mode; be sure to supply
    a 'fileType="b"' keyword argument if you are editing a binary file. Note
    that under Python 2.3 you can also specify 'fileType="U"' to use "universal
    newline" mode.
 
    'EditableFile' subclasses 'TxnFile', but does not use 'autocommit' mode,
    because it wants to support "safe" alterations to existing files.
}}} This looks pretty straightfoward to use, especially since we can
assume, since we are writing in a Data Manager, that we will be
inside a transaction and don't have to worry about that aspect here.
 
OK, now that we've got a way to transactionally update a file, we
need to exchange our `QueryDM` base class to a base class that
supports updating the database. That would be `EntityDM`.
 
`EntityDM` requires two additional methods to be defined by the
concrete class: `_new`, and `_save`. `_new` is called when a new
object is added to the DM, and needs to store the data for that
object in the external database. `_save` is called when an object's
state is changed, and a transaction boundry has been reached where
that state needs to be synchronized with the external database.
 
At this point our original scheme of reading and parsing the file
and storing the results the `data` attribute is going to break down.
With our current user interface it would actually ''work'' just
fine. The file would get reparsed on each command invocation. But
suppose we later rewrite the interface so that multiple commands
can be issued in the same run of the application? If we keep our
pre-parsed `data` attribute, a message updated by a `for` command
wouldn't be seen by subsequent `to` commands.
 
So, we're going to have to use the `EditableFile` instance as our
reference for each lookup from the database. Our new
`hello_storage.py` will now look like this: {{{
#!python
from peak.api import *
from peak.storage.files import EditableFile
from hello_model import Message
import re
 
class MessageDM(storage.EntityDM):
 
    defaultClass = Message
    fn = binding.Obtain(PropertyName('helloworld.messagefile'))
 
    def file(self):
        return EditableFile(filename=self.fn)
 
    file = binding.Make(file)
 
    def __makeRe(self, oid):
        return re.compile(r"^%s[|](.*)$" % oid, re.M)
 
    def __findMatch(self, oid, text):
        return self.__makeRe(oid).search(text)
 
    def _load(self, oid, ob):
        m = self.__findMatch(oid, self.file.text)
        return {'forname': oid, 'text': m.group(1)}
 
    def _new(self, ob):
        self.file.text += "%s|%s\n" % (ob.forname, ob.text)
 
    def _save(self, ob):
        self.file.text = self.__makeRe(ob.forname).sub(
            "%s|%s" % (ob.forname, ob.text), self.file.text)
 
}}} Well, that actually got a little simpler, didn't it? But I
notice that in my attempts to (temporarily) stay away from the
complexities of SQL, I seem to have wandered into the complexities
of regular expressions.
 
Let's get that out of the way first. The not-too-gnarly little
regular expression I use is designed to do two things: it matches
any line (that's the re.M bit) that starts with the oid followed
by a vertical bar, and when used for a search it returns a match
object that has a group containing everything after the vertical
bar to the next newline.
 
This regular expression is only going to work if there's no
space between the `forname` and the vertical bar, but since we're
now updating the database with our program as well as reading it,
we can insure that. If you are following along with running
code, though, you might want to edit the old `hello.list` to remove
the spaces.
 
There are a few things to notice about our revised DM.
 
We're still getting the filename from that same configuration
variable. Now, however, we are turning that into an `EditableFile`.
Again we use binding.Make to create a descriptor that will return
a real value (and cache it) when the class attribute is actually
accessed.
 
Note that without binding.Make here we'd be stuck, since if
we tried to do something like {{{
 
file = EditableFile(filename=fn)
 
}}} our application would blow up when we tried to access the
`EditableFile`, because it would have hold of the descriptor instance
instead of being able to access the filename ''through'' the
descriptor. (To PJE: I suspect I'm explaining this badly, because
I only half understand it; hopefully you can clarify it)
 
In the `_load` method the match group in the regex means the parsing
out of the message is already done for us, and all we have to do
is return the data.
 
The `_new` method simply appends an appropriate record to the file.
 
The `_save` method uses re's substitution method to generate
a new version of the string representing the file with
the body of the line matching the forname replaced by
the new value.
 
With this code in place our `for` method is working: {{{
 
% ./hello for Jeff: Hi, guy!
% ./hello to Jeff
Hi, guy!
 
}}}
 
 
== Oh no, a bug! ==
 
At this point certain readers are getting antsy because there's a
bug in the `for` method we created up above. Assuming you've got
all four of the entries in your database we've been using for
examples, what happens if you do this: {{{
 
% ./hello for Jeff: Long time, no see
 
}}} Obviously, the intent is to replace the current message for
Jeff with a new one. However, our `for` code assumed the `forname`
passed to it was a new name. Currently, the code will write a new
entry to the database, which because of our storage implementation
will append a new "Jeff" record to the end of the file. So techncially
we've got a bug in our database implementation, too.
 
Let's fix that problem first.
 
If a regex doesn't match, re will return None. So in our `_new`
method we can check to see if the new `forname` is already in the
database or not, and raise an error if it is: {{{
#!python
    def _new(self, ob):
        if self.__findMatch(ob.forname, self.file.text):
            raise KeyError, "%s is already in the database" % ob.forname
        self.file.text += "%s|%s\n" % (ob.forname, ob.text)
}}}
 
Now at least we'll get an error if we use our buggy
`for` subcommand: {{{
 
% ./hello for Jeff: Hello, dude.
  [...first part of traceback elided...]
  File "/usr/local/lib/python2.3/site-packages/peak/storage/data_managers.py", line 432, in flush
    oid = ob._p_oid = self._new(ob)
  File "/var/home/rdmurray/proj/peak/helloworld/07writabledb/hello_storage.py", line 27, in _new
    raise KeyError, "%s is already in the database" % ob.forname
KeyError: 'Jeff is already in the database'
 
}}}
 
Fixing the `for` command feels a little less "natural". In fact,
this is a limitation of the current DM framework that is slated to
be fixed in alpha 4.
 
For now one way to handle this is to implement a __contains__ for
the DM; {{{
#!python
    def __contains__(self, oid):
        return self.__findMatch(oid, self.file.text) is not None
 
}}} Armed with this, we can rewrite our `for` method as follows: {{{
#!python
    def _run(self):
        if len(self.argv)<2: raise commands.InvocationError("Missing arguments")
        parts = ' '.join(self.argv[1:]).split(':')
        if len(parts)!=2: raise commands.InvocationError("Bad argument format")
        forname = parts[0].strip(); message = parts[1].strip()
        storage.beginTransaction(self)
        if forname in self.Messages:
            msg = self.Messages[forname]
        else:
            msg = self.Messages.newItem()
            msg.forname = forname
        msg.text = message
        storage.commitTransaction(self)
 
}}}
 
With this change, updating the database works: {{{
 
% ./hello for Jeff: Hey, Dude!
% ./hello to Jeff
Hey, Dude!
 
}}}
 
= SQL, N2, and Naming =
 
(Note: This chapter has not yet been edited by PJE)
 
OK, the big moment has come (hold your chuckles!). Management is
impressed with our demo program, and they want us to take it into
production, for use in greeting customers. There's a wrinkle, though:
they aren't interested in having a custom greeting for every customer.
Instead, they want us to access the corporate PostgreSQL database,
check the group to which each customer has been assigned, and use
a greeting associated with the customer's group to greet the customer.
 
So in this chapter we're going to learn how to use PEAK facilities
to work with an SQL database, how to combine data from more than
one database but keep them all in sync, and we'll also get to see
just how flexible a PEAK program can be in the face of change.
Along the way we'll introduce PEAK's "n2" utility, and give you a
small glimpse of how powerful PEAK's naming system is. Oh, yeah,
and we'll exercise the configuration system a bit more, too.
 
Then we'll finish it all off by throwing away our version 1
demo program and writing version 2 to match the new reality.
 
== N2 ==
 
"N2" stands for "Namespace Navigator", and it is a general purpose
tool for browsing and manipulating any namespace for which someone has
written a navigator adapter. It is available as a subcommand of the
`peak` command: {{{
%peak n2
[n2] help
Available commands:
 
abort commit l ls mksub py rd rmsub unset
bind dir ll md mv python rm set
cd help ln mkdir pwd quit rmdir show
}}} Ty originally wanted to call it "into", so `peak n2` is also
a pun on "peek into".
 
Why is `n2` of interest to us now? One of the n2 adapters avaiable
is one that will allow us to talk to an SQL database. Not that
that's the way our program will talk to the corporate database,
mind. Rather, we want this tool to make it easy for us to set
up a test SQL database we can test our modidified program against
before we try it on the corporate database.
 
To do our testing, we'll use an SQLite database. (If you don't
have SQLite on your system, you can get it from
[http://www.hwaci.com/sw/sqlite/]. You'll also need the PySQLite
python interface library, available from
[http://sourceforge.net/projects/pysqlite/]. Or use your operating
system's package facility if it has one (for example, on FreeBSD
you could do "cd /usr/ports/database/py-PySQLite; make install").
 
Once these programs are installed, you can use n2 to work
with a test SQLite database: {{{
% peak n2 sqlite:test.db
1>
}}}
You'll note that the prompt is different this time. {{{
1> help
Available commands:
 
abort \abort \buf-load \echo \import \reset
commit \buf-append \buf-save \exit \python \rollback
go \buf-copy \buf-show \export \quit \set
help \buf-edit \commit \go \reconnect \sleep
rollback \buf-get \describe \help \redraw \source
}}} This n2 command interpreter is allowing you to enter SQL commands
as the normal thing. So most n2 commands are preceeded by a "\".
We can enter SQL commands, terminated by semi-colons, and they will
be applied against the "test.db" SQLite databse. (Don't worry
right now about how `sqlite:test.db` got turned into a connection
to an SQLite database, we'll come back to that in the next section).
 
What we need is a test table that maps customer names to customer
groups, since that's what we'll be accessing in the corporate
database. The schema for that database lists the table as
`custgroups`, and the columns of interest as `NAME` and `GRP`.
Both columns are strings, VARCHAR(40) and (20), respectively. That's
all we really need to know. In production our application will be
using the strings from the corporate database, so we can use any
old strings we like for our tests.
 
What we need to do is create a test table with the two
critical columns, and some test data stored in it: {{{
1> create table custgroups (NAME VARCHAR(40), GRP VARCHAR(20));
 
 
(0 rows)
1> insert into custgroups (NAME, GRP) VALUES ('Jeff', 'vip');
 
 
(0 rows)
1> insert into custgroups (NAME, GRP) VALUES ('Joe', 'peon');
 
 
(0 rows)
1> insert into custgroups (NAME, GRP) VALUES ('Klause', 'vip');
 
 
(0 rows)
1> insert into custgroups (NAME, GRP) VALUES ('Jackie', 'vvip');
 
 
(0 rows)
1> insert into custgroups (NAME, GRP) VALUES ('Fred', 'oridinary');
 
 
(0 rows)
select * from custgruops;
NAME GRP
-------------------- --------------------
Jeff vip
Joe peon
Klause vip
Jackie vvip
Fred ordinary
(5 rows)
1> commit
1> \quit
}}} Well, that was easy. Now we just have to tie it into our
program.
 
Note that the "commit" step, which is an n2 command and not an sql
command, is critical. N2 starts a transaction when it connects to
the database. The `commit` command causes n2 to tell the database
to commit the current transaction. If you don't do that, none of
the changes made to create the table and insert the data will
actually take effect.
 
 
== peak.ini, and Naming ==
 
Remember back at the beginning when we took a look at the usage
information for the peak command? The first argument to the peak
command is described as "NAME_OR_URL", where a name is something
defined in the `peak.running.shortcuts` section of an `ini` file,
and URL is "of a type supported by peak.naming".
 
Well, by now you should understand how to define something in
'peak.running.shortcuts' and what that accomplishes, since we used
it in the last chapter. It will thus probably come as no surprise
to you to find that the 'peak' command has a `ini` file, and in
that `ini` file there's a `peak.running.shortcuts` section. It
makes interesting reading when you've got few moments. You'll find
our friend `runIni` in there, as well as the `n2` we just learned
about.
 
Right now, though, we're interested in that second bit, the one
about the URL. PEAK provides a general way to refer to "resources"
via "names". Since "URL" stands for "Uniform Resource Locator",
it's a natural syntax for PEAK to use. A URL is divided into
multiple parts; we're only worried about the first two for now.
The first part is what comes before the first ":", and the second
part is what comes after. The first part is called the "scheme",
and the second is a path that, when interpreted in the context of
the scheme by the naming system, will yeild an object.
 
The peak command further requires that the referenced object support
one of the `peak.running' interfaces or be callable. That's
important: the naming system will look up anything it understands,
but something that uses the naming system to look things up may
have additional constraints on what referenced objects will actually
''work'' when specified in such a URL.
 
PEAK of course makes schemes and production rules completely
configurable. The `peak.naming.schemes` section of the
`peak.ini` file looks like this: {{{
[peak.naming.schemes]
 
# This section defines naming context factories or URL.Base subclasses to
# be used for various URL schemes. The entry name is the URL scheme, and
# the value is either the object or an import string for loading it.
 
import = "peak.naming.factories.peak_imports:importContext"
smtp = "peak.naming.factories.smtp:smtpURL"
uuid = "peak.naming.factories.uuid:uuidURL"
nis = "peak.naming.factories.nisns:nisURLContext"
config = "peak.naming.factories.config_ctx:PropertyContext"
 
ldap = "peak.storage.LDAP:ldapURL"
sybase = "peak.storage.SQL:GenericSQL_URL"
pgsql = "peak.storage.SQL:GenericSQL_URL"
psycopg = "peak.storage.SQL:GenericSQL_URL"
mockdb = "peak.storage.SQL:GenericSQL_URL"
gadfly = "peak.storage.SQL:GadflyURL"
sqlite = "peak.storage.SQL:SqliteURL"
cxoracle = "peak.storage.SQL:OracleURL"
dcoracle2 = "peak.storage.SQL:OracleURL"
 
logfile = "peak.running.logs:logfileURL"
logging.logger = "peak.running.logs:peakLoggerContext"
 
lockfile = "peak.running.lockfiles:lockfileURL"
nulllockfile = "peak.running.lockfiles:lockfileURL"
shlockfile = "peak.running.lockfiles:lockfileURL"
flockfile = "peak.running.lockfiles:lockfileURL"
winflockfile = "peak.running.lockfiles:lockfileURL"
win32.dde = "peak.storage.DDE:ddeURL"
 
http = "peak.naming.factories.openable:OpenableURL"
ftp = "peak.naming.factories.openable:OpenableURL"
https = "peak.naming.factories.openable:OpenableURL"
file = "peak.naming.factories.openable:FileURL"
pkgfile = "peak.naming.factories.openable:PkgFileURL"
icb = "peak.net.icb:ICB_URL"
 
 
 
tcp = "peak.net.sockets.tcpudpURL"
udp = "peak.net.sockets.tcpudpURL"
unix = "peak.net.sockets.unixURL"
unix.dg = "peak.net.sockets.unixURL"
fd.socket = "peak.net.sockets.fdURL"
 
zconfig.schema = "peak.config.load_zconfig:ZConfigSchemaContext"
shellcmd = "peak.naming.factories.shellcmd:ShellCommandCtx"
}}}
That's a lot of schemes. Take a look at that last one, `shellcmd'.
The peak command usage says we can use any scheme recognized by
peak.naming to provide the thing the peak command is going to
run. Presumably that means that we could tell it to run,
say, the "ls -l" command. Let's try it: {{{
% peak shellcmd:"ls -l"
total 15
-rwxr-xr-x 1 rdmurray wheel 272 Dec 7 22:04 hello
-rw-r--r-- 1 rdmurray wheel 159 Dec 7 22:04 hello.list
-rw-r--r-- 1 rdmurray wheel 201 Dec 7 22:04 hello_model.py
-rw-r--r-- 1 rdmurray wheel 1013 Dec 7 22:04 hello_model.pyc
-rw-r--r-- 1 rdmurray wheel 1097 Dec 7 22:04 hello_storage.py
-rw-r--r-- 1 rdmurray wheel 3308 Dec 7 22:04 hello_storage.pyc
-rw-r--r-- 1 rdmurray wheel 1519 Dec 7 22:04 helloworld.py
-rw-r--r-- 1 rdmurray wheel 2878 Dec 7 22:04 helloworld.pyc
}}} Well, how about that. Not, perhaps, a stunningly useful way
to run the "ls" command, but it certainly demonstrates the flexibility
of the PEAK naming system.
 
The `n2` subcommand we used in the last section also takes any
argument supported by `peak.naming`. The referenced object
must support a certain interface, but we'll ignore that for now.
 
When we used n2 to open a connection to our SQLite "test.db" database,
we were using the "sqlite" scheme listed above. You'll note that
there are several other database schemes supported by PEAK. That's
important, because as you'll recall the corporate database our
program ''really'' needs to connect to is Postgres. Fortunately,
`psycopg` is a scheme for accessing a Postgres database, so we're
all set.
 
In fact, if we actually had that corporate database to connect
to, we could do something like: {{{
% peak n2 psycopg://ouruserid:ourpassword@some.corporate.server.com/customerdb
}}} and run some queries against that table and find out what kind
of data they are really expecting us to interface with.
 
Now, this is all well and good, but so far it hasn't gotten us any
nearer making our program get group data from an SQL database. But
PEAK wouldn't have a naming system if it didn't use it everywhere,
of course, so you probably won't find it too surprising to that the
`naming.Obtain` function ''also'' accepts anything handled by
`peak.naming`, as long as it supports the `IComponentKey` interface
or can be adapted to it.
 
Hmm. "...or can be adapted to it." Now explaining ''that'' would
require quite a long digression. A very important digression if
you really want to understand the essence of PEAK, but one that is
far too long to get in to here. You can read
[http://www.python.org/peps/pep-0246.html PEP 246] and/or the
[http://peak.telecommunity.com/PyProtocols.html PyProtocols]
[http://peak.telecommunity.com/protocol_ref/module-protocols.html documentation]
if you want to delve into it now.
 
But all you really ''need'' to know for our purposes here is that
within the PEAK system we have a way of adding behavior to objects
in a very flexible way. So if we have an object that represents
a connection to a database, and Obtain wants it to support
`IConfigKey`, a little piece of code will make it do so in
a way transparent to us as users of that particular service.
What this means in practice is that in general when something
uses the naming system to look things up, and it seems reasonable
to use an object represented by a given URL, but you don't know
if that object supports whatever interface the component doing
the lookup requires, try it. There may be an adpater in
the system that will take care of it. And if it doesn't work,
but it still seems reasonable that it should, you can ''write''
an adapter so it will work. But that, as they say, is a topic
for another day. (Or if I can keep at this long enough,
another chapter.)
 
What this all means right now is that we can put something
in our `HelloWorld` class like this: {{{
db = Obtain("sqlite:test.db")
}}}
and like magic our program will have a connection to
our SQLite database.
 
Of course, that's just the beginning....
 
 
== Greater Configurability ==
 
Before we start accessing that database, let's think a little about
the fact that this is a test database we're working with. We know
we're going to have to switch which database we access eventually.
And that's something that could change in the future; after all,
they might move the database to some new machine someday.
 
So really we should put the pointer to the database into
the configuration file instead of burying it in the code.
The naming system provides a little utility for facilitating
this called `LinkRef`: {{{
[helloworld]
 
messagefile = config.fileNearModule('helloworld', 'hello.list')
db = naming.LinkRef('sqlite:test.db')
}}} So now we've got a "link" to a named resource stored in
our `helloworld.customerdb` configuration variable.
 
Hmm. But that isn't really quite what we want, either. After all,
our project group is working on other projects using PEAK, and some
of those will need to access to this same corporate database. It
would be a shame to have to go to the config file for each app and
adjust it if the database gets moved.
 
PEAK of course provides a way to deal with that. We can create a
special section in our `ini` file named `Load Settings From`, and
in that define a `file` variable pointing to another `ini` file.
(Footnote: actually, you might want to use `ZConfig` here rather
than an `ini` file, since `ZConfig` is probably better for "end
user" configuration tasks, but that's another topic I'm not ready
to delve in to.)
 
So we'll create a new file, `global.ini`: {{{
[corporate]
 
customerdb = naming.LinkRef('sqlite:test.db')
}}}
And at the top of our `hello` file: {{{
[Load Settings From]
file = config.fileNearModule('helloworld', 'global.ini')
}}} In our deployed application we'll actually want to
use either a hardcoded path name, or reference to a
module related to the shared parts of our corporate PEAK
applications.
 
This leads to another interesting point, though. If this
`global.ini` file were already deployed, and we needed some
of the settings in it, but we wanted to use our test database
rather than the real customerdb, what could we do? Naturally
enough, all it takes is to add a `[corporate]` section to
our `hello` config file, and define in there the config
variables we want to override.
 
 
== Reading from the SQL Database ==
 
We're finally ready to use the database.
 
Before we do this the right way, though, lets conduct a little
exercise. PEAK is designed to allow you to develop applications
that are flexible and easy to modify. Suppose we were trying
to do this database change on an emergency basis, and we wanted to
touch as little of our existing system as possible? Let's see what
we can do.
 
Our domain model pairs names with greetings. As far as the user
of the `to` subcommand goes, that's all that matters. So lets
see if we can maintain that pairing while using the data from
our new database.
 
First, we'll need a reference to the database as a class
variable in our DM. {{{
#!python
customerdb = binding.Obtain(PropertyName('corporate.customerdb'))
}}}
 
Next, we'll need to change our `_load` method to get the
right message, using auxiliary info from the customer
data base: {{{
#!python
    def _load(self, oid, ob):
        row = ~self.customerdb("select GRP from custgroups where NAME='%s'" %
            oid)
        m = self.__findMatch(row.GRP, self.file.text)
        msg = m.group(1) % oid
        return {'forname': oid, 'text': msg}
}}} Here you see that we can pass our SQL database object an SQL
query, and for a select get back a cursor into the database. Since
we're sure we're only going to get one record back, we use the unary
operator "oneOf" ("~") to pick it out. The "row" object we get
back has our select columns as attributes. So we get the data from
the `GRP` column for this user, and use ''that'' to look up our
message. You'll note that I'm back to doing "%" substitution on
the resulting message, so that we are in fact personalizing our
message for each customer. If corporate doesn't like that, it's
easy enough to back out.
 
Nothing else in our application ''has'' to change. Although
our usage message for the `for` subcommand is now wrong (it
sets the message for the group, not the thing being greeted),
it works just fine for storing messages into our database
under the group names: {{{
% ./hello for vip:"I'm so pleased to see you, %s"
% ./hello to Jeff
I'm so pleased to see you, Jeff
}}}
Another nice feature worth pointing out, even though it isn't useful
in our simple example, is that the SQL database object participates
in the transaction. (In fact, there has to be an active transaction
for the sql database function to work.) So if our subcommands
were in fact updating the SQL database, both the changes to it and
the changes to our `EditableFile` would be kept consistent across
PEAK transaction boundaries. If our PEAK transaction aborts,
''all'' database changes, to whichever database, are rolled back
(assuming the database supports transaction abort, of course).
 
And to go production, all we need to do is change that customerdb
declaration in the configuration file. The PEAK database object
hides all the details of the different databases from us. (Well,
as long as we can stick to standard SQL it does, anyway.)
 
If you think about what you just learned about how PEAK can be used
to tie together disparate, possibly legacy databases in the corporate
setting, and maintain enterprise-level control over the integrity
of the data, you can see why PEAK is an "''Enterprise'' Application
Toolkit".
 
 
== Doing it Right ==
 
All right, that incorrect usage message is bothering me. Let's fix
this application the right way.
 
Our domain model has actually changed: instead of the domain
consisting of "Message" objects that are associated with particular
things to greet, we now have "Customers" that belong to "Groups",
and it is the groups that have messages associated with them.
 
So, if we are going to bring our app in tune with the new
reality, we'll have to start by revising our domain model: {{{
#!python
from peak.api import *
 
 
class Group(model.Element):
 
    class name(model.Attribute):
        referencedType = model.String
 
    class text(model.Attribute):
        referencedType = model.String
 
 
class Customer(model.Element):
 
    class name(model.Attribute):
        referenceType = model.String
 
    class group(model.Attribute):
        referenceType = Group
}}}
 
The most interesting thing to notice about this new model is that
the reference type for the group attribute of our `Customer` is
actually the `Group` element we define. When we use this model,
we're going to actually store references to Group objects in the
group attribute of the Customer objects. But we'll do it the PEAK
way.
 
As long as we're rewriting anyway, we might as well get rid of that
clunky file and move to a real database for our messages. Perhaps
someday they'll move into a table on the corporate database, but
for now we'll stick with our SQLite database, since has proven its
worth so far.
 
We'll replace the `messagefile` configuration item in our `hello` file
with one for the new database: {{{
messagedb = naming.LinkRef('sqlite:messages.db')
}}}
 
Our `hello_storage.py` file will see the greatest amount of change.
Let's start with the simpler class, the Data Manager for the
Customers. This data is read only, so we can use `QueryDM` here: {{{
#!python
from peak.api import *
from hello_model import Customer, Group
 
class CustomerDM(storage.QueryDM):
 
    defaultClass = Customer
    customerdb = binding.Obtain(PropertyName('corporate.customerdb'))
    GroupDM = binding.Obtain(storage.DMFor(Group))
 
    def _load(self, oid, ob):
        row = ~self.customerdb("select GRP from custgroups where NAME='%s'" %
            oid)
        group = self.GroupDM[row.GRP]
        return {'name': oid, 'group': group}
}}} As you can see, there's some interesting stuff happening here.
 
First, we're using the binding system to pick up a reference to
the Data Manager for the group. Even though we don't define
that until later in the file, we don't have a reference problem,
because the dereference doesn't happen until runtime.
 
Second, we're returning an instance retreived from the `GroupDM`
as the value of one of our attributes. This kind of arrangement
is why Data Managers return ghosts: when we access a `Customer`
object, even though it contains a reference to a Group object,
no system activity (ie: reading from databases) happens because
of this until we actually try to reference an attribute of
the Group. Thus in PEAK inter-object links are not expensive,
even when the other objects are backed by potentially slow
external databases and/or occupy large amounts of memory when
active.
 
OK, here's the `GroupDM`, which is not any less complicated
than our old `MessageDM` from before we added the customers,
but which shows how to use SQL to do the same stuff: {{{
#!python
class GroupDM(storage.EntityDM):
 
    defaultClass = Group
    messagedb = binding.Obtain(PropertyName('helloworld.messagedb'))
 
    def _load(self, oid, ob):
        row = ~self.messagedb("select text from messages where name='%s'" %
            oid)
        return {'name': oid, 'text': row.text}
 
    def _new(self, ob):
        if ob.name in self:
            raise KeyError, "%s is already in the database" % ob.name
        self._save(ob)
 
    def _save(self, ob):
        self.messagedb(("insert or replace into messages (name, text) values "
            "('%s', '%s')") % (ob.name, ob.text))
 
    def __contains__(self, oid):
        #using where would be more efficient here, but I don't
        #know how to test the cursor to see if I got any results
        for row in self.messagedb('select name from messages'):
            if row.name==oid: return 1
        return 0
}}}
 
Now, of course, our application needs to use the new domain
objects: {{{
#!python
from peak.api import *
from hello_model import Customer, Group
 
class HelloWorld(commands.Bootstrap):
 
    usage = """
Usage: hello command arguments
 
Available commands:
 
    for -- sets a greeting
    to -- displays a greeting
"""
 
    CustomerDM = binding.Make('hello_storage:CustomerDM',
        offerAs=[storage.DMFor(Customer)])
    GroupDM = binding.Make('hello_storage:GroupDM',
        offerAs=[storage.DMFor(Group)])
    #Does this actually do anything useful?:
    CustomerDB = binding.Obtain(PropertyName('corporate.customerdb'),
        offerAs=[PropertyName('corporate.customerdb')])
    MessageDB = binding.Obtain(PropertyName('helloworld.messagedb'),
        offerAs=[PropertyName('helloworld.messagedb')])
 
 
class toCmd(commands.AbstractCommand):
 
    usage = """
Usage: hello to <name>
 
Displays the greeting for "name".
"""
 
    Customers = binding.Obtain(storage.DMFor(Customer))
 
    def _run(self):
        if len(self.argv)<2: raise commands.InvocationError("Missing name")
        storage.beginTransaction(self)
        print >>self.stdout, self.Customers[self.argv[1]].group.text
        storage.commitTransaction(self)
 
 
class forCmd(commands.AbstractCommand):
 
    usage = """
Usage: hello for <group>: <greeting>
 
Stores "greeting" as the greeting message for group "group".
"""
 
    Groups = binding.Obtain(storage.DMFor(Group))
 
    def _run(self):
        if len(self.argv)<2: raise commands.InvocationError("Missing arguments")
        parts = ' '.join(self.argv[1:]).split(':')
        if len(parts)!=2: raise commands.InvocationError("Bad argument format")
        groupname = parts[0].strip(); message = parts[1].strip()
        storage.beginTransaction(self)
        if groupname in self.Groups:
            group = self.Groups[groupname]
        else:
            group = self.Groups.newItem()
            group.name = groupname
        group.text = message
        storage.commitTransaction(self)
}}}
 
The changes here are relatively small, almost trivial. The biggest
change is that we are getting the message from the text attribute
of the `group` attribute of a `Customer` object. The changes in
`for` are mostly to the usage string and some variable name changes,
just to be consistent.
 
If we test this program, though, we'll quickly find a design error: {{{
% ./hello for vip:"I am so happy to see you, %s"
% ./hello to Jeff
I am so happy to see you, %s
}}} Woops, we forgot about our tailored messages.
 
How to solve this? Well, clearly a tailored message is more a
property of the `Customer` proper. So we can add something to
our domain model: a `greeting` function. And we can take
advantage of this to deal with the case where our bosses are
serious about not wanting tailored messages. We'll allow
for messages that don't have a substitution point: {{{
%!python
class Customer(model.Element):
 
    class name(model.Attribute):
        referenceType = model.String
 
    class group(model.Attribute):
        referenceType = Group
 
    def greeting(self):
        if '%' in self.group.text:
            return self.group.text % self.name
        else: return self.group.text
}}}
 
Now our `print` line in `helloworld.py` becomes:{{{
print >>self.stdout, self.Customers[self.argv[1]].greeting()
}}} Which is more Demeter-proof anyway.
 
And now everyone is happy:{{{
% ./hello to Jeff
I am so happy to see you, Jeff
% ./hello for vvip:"Greetings, Your Excelency!"
% ./hello to Jackie
Greetings, Your Excelency!
}}}
 
 
= Logging =
 
(This chapter has not be edited by PJE)
 
So, our application is deployed. People are using it. But who?
And how much? And just who was it who changed the greeting
for vvips to "Your most humble servant greets you,
Grand and Mighty Poobah!"? Management would like to know.
 
Well, we can't help them with the last question, because
that's already in the past. But we can start gathering the
data needed to answer these sorts of questions, using
PEAK's logging facilities. So in this chapter, we'll
introduce logging, and show how to incorporate it into
our program.
 
 
== Generating Log Messages ==
 
Keeping with the PEAK philosophy of making things easily
configurable, we're not going to hardcode where our
log messages go. Instead our code will simply ask
the configuration system for a logging object, and
pass that object the log messages.
 
A logger in PEAK is something that provides the `ILogger` interface.
Let's take a look: {{{
% peak help running.ILogger
Help on class ILogger in module peak.running.interfaces:
 
class ILogger(protocols.interfaces.Interface)
 | A PEP 282 "logger" object, minus configuration methods
 |
 | All methods that take 'msg' and positional arguments 'args' will
 | interpolate 'args' into 'msg', so the format is a little like a
 | 'printf' in C. For example, in this code:
 |
 | aLogger.debug("color=%s; number=%d", "blue", 42)
 |
 | the log message will be rendered as '"color=blue; number=42"'. Loggers
 | should not interpolate the message until they have verified that the
 | message will not be trivially suppressed. (For example, if the logger
 | is not accepting messages of the designated priority level.) This avoids
 | needless string processing in code that does a lot of logging calls that
 | are mostly suppressed. (E.g. debug logging.)
 |
 | Methods that take a '**kwargs' keywords argument only accept an 'exc_info'
 | flag as a keyword argument. If 'exc_info' is a true value, exception data
 | from 'sys.exc_info()' is added to the log message.
 |
 | Method resolution order:
 | ILogger
 | protocols.interfaces.Interface
 | __builtin__.object
 |
 | Methods defined here:
 |
 | critical(msg, *args, **kwargs)
 | Log 'msg' w/level CRITICAL
 |
 | debug(msg, *args, **kwargs)
 | Log 'msg' w/level DEBUG
 |
 | error(msg, *args, **kwargs)
 | Log 'msg' w/level ERROR
 |
 | exception(msg, *args)
 | Log 'msg' w/level ERROR, add exception info
 |
 | getEffectiveLevel(lvl)
 | Get minimum priority level required for messages to be accepted
 |
 | info(msg, *args, **kwargs)
 | Log 'msg' w/level INFO
 |
 | isEnabledFor(lvl)
 | Return true if logger will accept messages of level 'lvl'
 |
 | log(lvl, msg, *args, **kwargs)
 | Log 'msg' w/level 'lvl'
 |
 | warning(msg, *args, **kwargs)
 | Log 'msg' w/level WARNING
 |
 | ----------------------------------------------------------------------
 | Data and other attributes inherited from protocols.interfaces.Interface:
 |
 | __dict__ = <dictproxy object>
 | dictionary for instance variables (if defined)
 |
 | __metaclass__ = <class 'protocols.interfaces.InterfaceClass'>
 |
 | __weakref__ = <attribute '__weakref__' of 'Interface' objects>
 | list of weak references to the object (if defined)
 
}}} Hmm. There's a lot of information and capabilities there. We're
interested in the general format of a logging call, and which logging
calls are provided. Looking through the list of methods, things
like `critical`, 'debug`, `warning`, `error`, and `info` all look
handy. The other methods we can ignore for now.
 
So let's modify our `to` command to log who is greeting whom (I'm
going to omit the `usage` text when I show modified versions
of our commands from now on; you've seen that text often
enough already!): {{{
#!python
class toCmd(commands.AbstractCommand):
 
    Customers = binding.Obtain(storage.DMFor(Customer))
    log = binding.Obtain(PropertyName("helloworld.logger"))
 
    def _run(self):
        if len(self.argv)<2: raise commands.InvocationError("Missing name")
        storage.beginTransaction(self)
        print self.Customers[self.argv[1]].greeting()
        self.log.info("%s issued greeting to %s", self.environ['USER'],
            self.argv[1])
        storage.commitTransaction(self)
}}}
 
We've added two lines here. The first sets up a class variable `log`
and binds it to the configuration key `helloworld.logger`. We'll
talk about what we set that key to in the next section. The second
line generates the actual log entry. As you can see, we call the
`info` method of our `ILogger` instance, and pass it a message
containing substitution points, and the values to be substituted.
 
Now let's consider our `for` command: {{{
#!python
class forCmd(commands.AbstractCommand):
 
    Groups = binding.Obtain(storage.DMFor(Group))
    log = binding.Obtain(PropertyName("helloworld.logger"))
 
    def _run(self):
        if len(self.argv)<2: raise commands.InvocationError("Missing arguments")
        parts = ' '.join(self.argv[1:]).split(':')
        if len(parts)!=2: raise commands.InvocationError("Bad argument format")
        groupname = parts[0].strip(); message = parts[1].strip()
        storage.beginTransaction(self)
        if groupname in self.Groups:
            group = self.Groups[groupname]
            self.log.warning('Greeting for group %s changed to "%s" by %s',
                groupname, message, self.environ['USER'])
        else:
            group = self.Groups.newItem()
            group.name = groupname
            self.log.warning('Greeting ("%s") for group %s added by %s',
                message, groupname, self.environ['USER'])
        group.text = message
        storage.commitTransaction(self)
}}}
 
Again we set up the logger. Here we have two possible logged messages,
depending on whether or not this is the first time a message has
been set for the group.
 
This time we're logging at `warning` level, figuring that messages
changes should be relatively rare, and if someone is monitoring the
logs that's probably what they'll be interested in seeing. As
implied by the help information up above, PEAK provides ways of
filtering the generated log messages based on `priority`, of which
`info` and `warning` are two examples. This way, if all we need
to know is who is changing the messages, we can set PEAK to ignore
the `info` messages generated by uses of the `to` command.
 
 
== Logging to the Terminal ==
 
So how do we tell PEAK what we want to log, and where? We do that
by specifying those details when we define what goes in to the
`helloworld.logger` configuration variable.
 
To start with, let's configure the logging information to be
dumped to `stderr`, so we can test our logging code and make
sure it is doing what we want. And since we're testing,
we're going to want to see messages of any priority. `debug`
is the lowest priority, so we'll tell the logger to log
everything from `debug` on up. (We aren't using any
debug calls now, but we might want to add some later
next time we have to do some debugging of our application.)
 
{{{
[helloworld]
 
messagedb = naming.LinkRef('sqlite:messages.db')
logger = logs.LogStream(stream=importString('sys.stderr'), level=logs.DEBUG)
}}}
 
Here we are using PEAK's `LogStream` logger. To work, it needs
to know which stream to write to, so we tell it using
`stream=importString('sys.stderr')`. We need to use `importString`
because `sys.stderr` is an import path. We also tell the logger
the minimum level of message to log. (Message levels are named the
same as the corresponding method names, but using all caps).
 
Let's try it out: {{{
% ./hello to Jeff
I am so happy to see you, Jeff
Dec 09 19:34:12 stage PEAK[12865]: rdmurray issued greeting to Jeff
% ./hello for peon: "Hi, %s"
Dec 09 19:36:49 stage PEAK[12870]: Greeting ("Hi, %s") for group peon added by rdmurray
% ./hello for peon: "Hi, scum"
Dec 09 19:37:49 stage PEAK[12871]: Greeting for group peon changed to "Hi, scum" by rdmurray
}}} As you can see, the logging system automatically adds a timestamp, the
system name, an application name, and a processid, very similar to the
way the unix syslog works.
 
== Other Logging Destinations ==
 
OK, so everything is working. We don't want these messages appearing
on the console when we roll out this new version, so we'd better
redirect the logs. We could have them go to a file: {{{
logger = naming.LinkRef("logfile:helloworld.log?level=INFO")
}}} Lots of different users are using this program, though, and
we don't really want log files in whatever directory they happen
to run the command from. We could specify an absolute path, but
it would have to be one that all the potential users could write
to. This is a practical solution: just have the file be writable
by a group all the users belong to.
 
Many systems have a simpler solution, though, and PEAK supports it:
syslog. Here's how we'd direct our logging to syslog: {{{
logger = naming.LinkRef("syslog:local7")
}}} Here we're telling the logger to send the log messages to
syslog under the `local7` facility. We don't specify a log level,
because that can be controlled by the syslog configuration. That
of course means that PEAK must fully generate and send all
messages, which is an overhead worth thinking about if you
select syslog logging. If you don't want that overhead,
you can specify a level just like for the logfile logger,
and PEAK will only syslog messages at that level and higher.
 
== Multiple Log Destinations ==
 
The Boss is really worried about that unauthorized message change.
He wants to be notified immediatly of any new message changes.
Systems has set up a unix named pipe that they've tied to their
paging system, so anything written to that pipe will get sent to
the boss's pager. But we still need to keep logs of all access,
for those usage statistics he also wants. So we can't just redirect
logging, at `warning` level, to the pipe. We need to send the
logging info to two destinations, with two different filtering
levels.
 
And here's how we do it: {{{
logger = logtee:{syslog:local7},{logfile:/tmp/pageboss?level=WARNING}
}}}
 
(NB: Neither syslog: nor logtee: are currently hooked up in
peak.ini, so these examples don't work/aren't tested)
 
= Exploring the rest of PEAK =
 
One important topic we haven't covered here is the "component hierarchy", although we've alluded to it on occasion by talking about "parent" and "child" components. Generally speaking, a component contained within another component is a "child" component of the container, and PEAK provides mechanisms for components to access their containers to "obtain" configuration or other components. It's also possible to treat the component hierarchy like a kind of file system, using paths to navigate from one component to another.
 

PythonPowered
ShowText of this page
EditText of this page
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, DeletePage, LikePages, LocalSiteMap, SpellCheck