[PEAK] discovering peak.web

alexander smishlajev alex at ank-sia.com
Fri Feb 4 14:26:12 EST 2005


hello!

lately i was trying to find out what peak.web is and how it ticks.  i 
suppose that my examples may make a wiki page if i am doing nothing 
fatally wrong in there.  i tried to follow MoinMoin syntax, but i didn't 
check the contents with real wiki, so there may be markup errors.

-----

== Content producer ==

Suppose we have an existing application component and we want to make it 
accessible through the web.

The following example shows simple component producing an infinite 
series of text slides:

{{{
#!python
import sys
from urllib import quote

import protocols
from peak.api import binding, config, naming, security, web
from peak.running.interfaces import IRerunnable
from peak.tools.local_server import WSGIServer

class ISlides(IRerunnable):

     """Infinite sequence of slides"""

     binding.metadata(
         next=security.Anybody,
         current=security.Anybody,
     )

     next = protocols.Attribute("""Next slide.

     This is a generator property, making new contents
     each time it is evaluated.

     """)

     current = protocols.Attribute("""Current slide.""")

class Slides(binding.Component):

     protocols.advise(instancesProvide=[ISlides])

     def slide():
         yield "Episode 12B"
         yield "How to recognise different types of trees" \
             " from quite a long way away"
         slideno = 0
         while True:
             slideno += 1
             yield "No %i" % slideno
             yield "The Larch"
             yield "And Now..."
     slide = binding.Make(slide)

     current = binding.Make(str)

     def next(self):
         self.current = self.slide.next()
         return self.current
     next = property(next)

     def run(self, stdin, stdout, stderr=None, environ={}, argv=[]):
         stdout.write("%s\n" % self.next)

def main():
     root = config.makeRoot()
     cmd = Slides(root)
     for ii in xrange(10):
         cmd.run(None, sys.stdout)

if __name__ == "__main__":
     main()
}}}

Note that there are many unused imports in this script; that's 
intentional, because examples in the following sections are 
modifications of this script, and i want to skip the common part in 
those examples.  (Of course, you wouldn't copy reusable code from script 
to script in the real life!)

=== Simple WSGI handler ===

We start with simple wrapper component, itself doing most of the request 
processing.  {{{peak.web.Location}}} seems to be a good base for such 
wrapper: request path traversal and security rules will be processed by 
PEAK, so we can focus on our application.

When HTTP request path ends at our ''location'', PEAK looks for default 
request handler which name is given in config setting 
{{{peak.web.defaultMethod}}} and usually is {{{index_html}}} (i.e., for 
example {{{http://my.host/}}} works like an alias for 
{{{http://my.host/index_html}}}).  At the same ''location'', we could 
have other handlers with other names too; they would be accessible by 
URLs with handler name put instead of {{{index_html}}}.

We must have our handler accessible by web users.  We do this by 
declaring that access to {{{index_html}}} is controlled by security rule 
{{{Anybody}}}, i.e. this method is accessible for everyone.

We use fixed address and port for our HTTP server.  Contents can be 
viewed at {{{http://localhost:8917/}}}.

{{{
#!python

  ...

class SlideShow(web.Location):

     binding.metadata(index_html=security.Anybody)

     slides = binding.Make(Slides)

     def index_html(self, ctx):
         text = self.slides.next
         # Return '(status,headers,output_iterable)' tuple
         return ('200 OK', [('Content-Type', 'text/plain'),
             ('Content-Length', str(len(text)))], [text])

def main():
     root = config.makeRoot()
     server = WSGIServer(root, socketURL="tcp://localhost:8917/")
     server.cgiCommand=SlideShow(server)
     server.run()

if __name__ == "__main__":
     main()
}}}

== Using page templates ==

In previous example, our {{{index_html}}} was rendered by custom python 
code.  Following script uses PEAK web templates to render dynamic content:

{{{
#!python

  ...

class SlideShow(web.Location):

     binding.metadata(
         index_html = security.Anybody,
         slides = security.Anybody,
     )

     slides = binding.Make(Slides)

     TEMPLATE = """<html this:is="page" with:content-type="text/html">
  <body>
   <h1 content:replace="slides/next" />
  </body>
</html>
"""

     index_html = binding.Make(lambda self: config.processXML(
         web.TEMPLATE_SCHEMA(self.slides),
         "data:," + quote(self.TEMPLATE),
         pwt_document=web.TemplateDocument(self.slides)
     ))

def main():
     root = config.makeRoot()
     server = WSGIServer(root, socketURL="tcp://localhost:8917/")
     server.cgiCommand=SlideShow(server)
     server.run()

if __name__ == "__main__":
     main()
}}}

In real applications, templates usually come from disk files; in this 
example, we put template text into the program body to simplify example 
setup.

Insertion of dynamic content is controlled by "magic" element attributes 
in namespaces {{{this}}}, {{{content}}} and {{{with}}}.  {{{this}}} 
means we want to do something with the element holding the attribute, 
{{{content}}} specifies actions on the contents of the element, and 
{{{with}}} provides parameters for the element rendering.

Namespaces {{{this}}} and {{{content}}} out-of-the box allow following 
attribute names:

   * {{{is}}} - asserts that contents are known under the name set by 
attribute value.  E.g. {{{<li this:is="listItem" content:replace="title" 
/>}}} sets {{{listItem}}} template for {{{list}}} renderer (see below).
   * {{{replace}}} - contents (whole element for {{{this}}} or element 
contents for {{{contents}}}) is replaced by result of path traversal; 
attribute value is path.
   * {{{xml}}} - contents are replaced by result of path traversal 
without escaping XML markup.
   * {{{list}}} - render a sequence of values.  This DOMlet accepts 
parameters {{{listItem}}}, {{{header}}}, {{{emptyList}}} and 
{{{footer}}}.  {{{listitem}}} is rendered for each element of the list, 
{{{emptyList}}} is rendered when there are no elements in the list, and 
{{{header}}} and {{{footer}}} are inserted at start and end of the list, 
respectively.
   * {{{uses}}} - render child elements with target data, or skip 
element altogether.  Attribute value is traversal expression specifying 
the starting point for path traversals in child elements.
   * {{{unless}}} - contents are rendered only if target data is not 
available.
   * {{{expects}}} - an assertion as to the type or protocol of the 
thing currently being handled.  Attribute value is protocol import 
specifier; DOMlet value will be adapted to that protocol at the time of 
rendering.

Names of attributes in namespace {{{with}}} are names of parameters for 
operating DOMlet.  In the above example, {{{content-type}}} is a 
parameter of HTML page renderer.

== Adding your own DOMlets ==

You may develop custom content handlers and register them for use in 
{{{this:}}} and {{{content:}}} attributes of template elements.  In the 
following example, we display the tree image when our slide refers to 
the larch:

{{{
#!python

  ...


class SlideShow(web.Location):

     binding.metadata(
         index_html = security.Anybody,
         slides = security.Anybody,
     )

     slides = binding.Make(Slides)

     TEMPLATE = """<html this:is="page" with:content-type="text/html">
  <body this:uses="slides/next" >
   <h1 content:replace="." />
   <p this:when-larch=".">
    <img src="http://www.ank-sia.com/~alex/larch.jpg" />
   </p>
  </body>
</html>
"""

     index_html = binding.Make(lambda self: config.processXML(
         web.TEMPLATE_SCHEMA(self.slides),
         "data:," + quote(self.TEMPLATE),
         pwt_document=web.TemplateDocument(self.slides)
     ))

class WhenLarch(peak.web.templates.Element):

     """Render only if traversal result contains word 'Larch'"""

     # enable dynamic contents
     staticText = None

     def renderFor(self, data, state):
         if self.dataSpec:
             (td, ts) = self._traverse(data, state)
             if "Larch" in td.current:
                 state.write(self._openTag)
                 for child in self.optimizedChildren:
                     child.renderFor(data,state)
                 state.write(self._closeTag)

def main():
     root = config.makeRoot()
     root.registerProvider("peak.web.verbs.when-larch",
         config.Value(peak.web.templates.negotiatorFactory(WhenLarch)))
     server = WSGIServer(root, socketURL="tcp://localhost:8917/")
     server.cgiCommand=SlideShow(server)
     server.run()

if __name__ == "__main__":
     main()
}}}

Normally, DOMlet handlers are registered in section {{{peak.web.werbs}}} 
of PEAK configuration file like this:

{{{
[peak.web.verbs]
when-larch = pwt.negotiatorFactory(my.module.WhenLarch)
}}}

Again, we did that in program code to make example setup more simple.

== Sitemaps ==

Real applications are often much more complex than just a single web 
page, like one shown in above examples.  Here come sitemaps, allowing 
you to build a site out of collection of components, templates and 
resources.

Our setup in this example is also more complex.  First, we make python 
package directory for web resources (templates and static files):

{{{
$ mkdir templates
$ touch templates/__init__.py
}}}

In this directory, we create template file {{{slide.pwt}}}:

{{{
<html this:is="page" with:content-type="text/html">
  <body>
   <h1 content:replace="next" />
  </body>
</html>
}}}

Next, we must allow access to that package from peak.web.  We create 
file named {{{resources.ini}}} (in the current directory) with the 
following contents:

{{{
[peak.web.resource_packages]
templates = True
}}}

Finally, we make sitemap file (named {{{sitemap.xml}}}) itself:

{{{
<location id="root" config="resources.ini">
     <container object="Slides()" />
     <content type="ISlides">
	<view name="index_html" resource="templates/slide"/>
         <view name="larch" resource="templates/larch.jpg"/>
     </content>
</location>
}}}

In this example, we have only one (root) location with two views: 
{{{index_html}}}, displaying our application component, and {{{larch}}}, 
publishing a static file from resource package.

And now... the script to run all this:

{{{
#!python

  ...

def main():
     root = config.makeRoot()
     server = WSGIServer(root, socketURL="tcp://localhost:8917/")
     server.cgiCommand=config.processXML(web.SITEMAP_SCHEMA(server),
         "sitemap.xml", parent=server, sm_globals=globals())
     server.run()

if __name__ == "__main__":
     main()
}}}

There is near to zero web code in this script!  If our sitemap knew 
where to take the component it publishes (i.e. if we used valid import 
specifications in {{{container object}}} and {{{content type}}}), we 
wouldn't need our launch script at all.  Sitemap may be run by itself 
with PEAK command line tool:

{{{
$ peak serve ref:sitemap at file:sitemap.xml
}}}

or, to automatically launch web browser displaying our application:

{{{
$ peak launch ref:sitemap at pkgfile:my.module/sitemap.xml
}}}

(in the later example sitemap is located in python package {{{my.module}}}).

-----

best wishes,
alex.




More information about the PEAK mailing list