[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