The PEAK Developers' Center   PeakWebHowTo UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View

This is an informal introduction to peak.web written after exercises taken to learn peak.web functionality.

Content producer

Suppose we have an existing application component which we want to make accessible over the web.

This shows a simple component producing an infinite series of text slides. Create the file slides.py and paste this content:

#!python
import sys
from urllib import quote

import protocols
import peak.web.templates
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(self):
        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.verbs 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.

Finally we need to have the current directory in an importable path:

$ export PYTHONPATH=.

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. To do so we first need to change our sitemap.xml:

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

Notice the import of the module slides, wich is our original slides.py. Next we need to start the server using the sitemap.xml, we use the command serve to do it. There is a special flag -p <PORT> to specify the port we want our webserver to listen to. Finally we run peak as the following:

$ peak serve -p 8917 ref:sitemap@file:sitemap.xml

If the sitemap was located inside a Python package directory we could specify it using a pkgfile: URL in place of a file: URL. For example, if the sitemap file was in the package directory for a package named some.package, we could launch the web browser displaying our application:

$ peak launch ref:sitemap@pkgfile:some.package/sitemap.xml

(The peak serve command just starts a web server. The peak launch command is similar, but also launches a web browser to display the root of the application -- a convenient development tool.)


PythonPowered
EditText of this page (last modified 2007-03-26 19:17:30)
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, DeletePage, LikePages, LocalSiteMap, SpellCheck