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-07 01:15:47 and 2003-12-09 22:22:19 (spanning 28 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!"
 

class HelloWorld(commands.AbstractCommand):
 
    def _run(self):
        print "Hello, world!"
        print >>self.stdout, "Hello, world!"
 
}}} Hmm, what's that output redirection? Doesn't print normally
write to `stdout`?
 
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.
 
(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
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.

    message = binding.Obtain(PropertyName('helloworld.message'))
 
    def _run(self):
        print self.message
        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

 
  * 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.)

    message = binding.Obtain(PropertyName('helloworld.message'))
 
    def _run(self):
        print self.message % self.argv[1]
        print >>self.stdout, self.message % self.argv[1]
 
}}} Note the use of `self.argv` to get access to the command
arguments.
 
}}} The corresponding `hello` file would look like this: {{{
The corresponding `hello` file would look like this: {{{
 
#!/usr/bin/env peak runIni
 

Hello, world. How are you today?
% ./hello fred
Hello, fred. How are you today?
% ./hello, Klause
% ./hello Klause
Hello, Klause. How are you today?
 
}}}

 
All that remains, then, is to use our new data manager from our main application
program: {{{
 
#!python
from peak.api import *
from hello_model import Message

 
    def _run(self):
        storage.beginTransaction(self)
        print self.Messages[self.argv[1]].text
        print >>self.stdout, self.Messages[self.argv[1]].text
        storage.commitTransaction(self)
 
}}} As you can see, our main program has stayed fairly simple,

 
= Multiple Commands and Writable Databases =
 
== Introduction ==
(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 is demonstrating.
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

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 implementation of this class, `Bootstrap`, that we can reuse
to our advantage. So, our `AbstractCommand` from the previous
examples is now going to become the implementation for one of
our two subcommands. We'll have a new object that we'll be
having peak call as our "main program": {{{
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

"""
 
    Messages = bindings.Make(
        'hello_storage:MessageDM',
        offerAs=[storage.DMFor(Message)]
        )
        'hello_storage.MessageDM', offerAs=[storage.DMFor(Message)]
    )
 
 
class toCmd(commands.AbstractCommand):

Displays the greeting for "name".
"""
 
    Messages = binding.Obtain(storage.DMFor(Messages))
    Messages = binding.Obtain(storage.DMFor(Message))
 
    def _run(self):
        storage.beginTransaction(self)
        print self.Messages[self.argv[1]].text
        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. PEAK will make the subcommands children
of our `AbstractInterpreter`, so the subcommands will have this DM
binding in their context.
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

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 referenced by the
descriptor in the `HelloWorld` class, and as will be referenced by
our other subcommand once we write it.
 
Well, it will once we hook it up so that it can actually be invoked
as a subcommand.
 
To do that, we create a new binding. We could do this in our python
code, but we'll do it in the configuration file to show how
flexible PEAK commands can be (imagine tailoring a version of a
command to have a restricted set of subcommands for use by a
particular user group just by creating a different config file to
call it through): {{{
'''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'.
 
[peak.running.shortcuts]
}}}
 
to = importString('helloworld:toCmd')
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')
 
}}}
 
With the above added to our `hello` file, we can now
do things like: {{{
and now try it: {{{
 
    % ./hello to Fred
    Greetings, good sir.
 
}}}
 
Here's a slightly subtle point you may not have noticed: when we
turned our command into a subcommand we did ''not'' need to change
our argv index number. The argv 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]`.
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 try this: {{{
Now for something completely different. Let's try this: {{{
 
    % ./hello to
 

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`, though:
display the usage if we raise a `commands.InvocationError`, in our
`_run` method, though:
{{{
 
#!python
def _run(self):
    if len(self.argv)<2: raise commands.InvocationError("Missing name")
    storage.beginTransaction(self)
    print self.Messages[self.argv[1]].text
    storage.commitTransaction(self)
 
    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: {{{

 
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 Subcommand ==
== 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

completely independent of how we go about initiating the save
in the application program.
 
So, we need another `AbstractCommand` subclass, and another
binding in our `hello` config file: {{{
So, we need another
rule in our `hello` configuration file: {{{
 
[peak.running.shortcuts]
* = commands.NoSuchSubcommand
to = importString('helloworld.toCmd')
for = importString('helloworld.forCmd')
 
to = importString('helloworld:toCmd')
for = importString('helloworld:forCmd')
 
}}}
}}} and another `AbstractCommand` subclass in `helloworld.py`
 
{{{
 
#!python
class forCmd(commands.AbstractCommand):
 
    usage = """

    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(':')
        if len(parts)!=2: raise commands.InvocationError("Bad argument format")
        forname = parts[0].strip(); message = parts[1].strip()
        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
        newmsg.text = message
        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

 
Stores "greeting" as the greeting message for "name".
 
 
for: Missing arguments
 
% ./hello for foobar

 
Stores "greeting" as the greeting message for "name".
 
 
for: Bad argument format
 
% ./hello for Jeff: Hi, guy!

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

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):

  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,

 
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
#!python
    def _run(self):
        if len(self.argv)<2: raise commands.InvocationError("Missing arguments")
        parts = ' '.join(self.argv[1:]).split(':')

 
}}}
 
== Exploring the rest of PEAK ==
= 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