In a web service, you are free to return whatever set of responses formats you so choose; however, plain text, JSON, and HTML are probably the most common
| Format | Human-friendly | Computer-friendly |
| HTML | Yes | Yes, if structured |
| JSON | No | Yes |
| text/plain | Yes | Maybe |
Often, a web service may convey information in more than one format. (Example: /portfolio/process.gv.txt, /portfolio/process.gv.txt?format=raw, /portfolio/process.gv.txt?format=svg; you could also use the Accept: header, but you can't make a permalink this way ) (Another example: /my-service/foo.json, /my-service/foo.txt, /my-service/foo.html, etc.)
JSON == JavaScript Object Notation
Returning JSON in python is easy:
>>> import json
>>> foo = { 'hello': 1, 'foobar': 2, 'fleem': {'nested': True, 'error': None}}
>>> json.dumps(foo)
'{"fleem": {"error": null, "nested": true}, "foobar": 2, "hello": 1}'
In a web service, you would just return this as the response.
In recent versions of python, the json module is part of the standard library. In older versions, you will have to easy_install simplejson
How to support both json and simplejson:
try: import json except ImportError: import simplejson as json
You should also add simplejson to the install_requires section of your setup.py
Of course, you can only use json.dumps() to serialize objects that are supported in JSON, such as strings, numbers, dictionaries, arrays, True, False, and None. Try to do this with other objects will result in an error.
>>> import sys
>>> json.dumps(sys.stdin)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.6/json/__init__.py", line 230, in dumps
return _default_encoder.encode(obj)
File "/usr/lib/python2.6/json/encoder.py", line 367, in encode
chunks = list(self.iterencode(o))
File "/usr/lib/python2.6/json/encoder.py", line 317, in _iterencode
for chunk in self._iterencode_default(o, markers):
File "/usr/lib/python2.6/json/encoder.py", line 323, in _iterencode_default
newobj = self.default(o)
File "/usr/lib/python2.6/json/encoder.py", line 344, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <open file '<stdin>', mode 'r' at 0xb78c2020> is not JSON serializable
Loading JSON is doable with json.loads, but it is picky
A template is a prototype of a document that will be rendered with variables (and possibly logic) to create a final document.
Let's say you have a python file, that you want to package. We'll use a8e.py from last class. But before we start ...
Why bother creating a python package?
If you're just going to run a script from the command line, then you might not want to package it. However, making a real package has certain advantages:
So how do I create one?
PasteScript comes with a basic_package template out of the box. Once you easy_install PasteScript, it should be available to you.
Making a8e.py into a package:
>>> from a8e import a8e
>>> a8e.__file__
'/home/jhammel/stage/src/a8e/a8e/a8e.py'
>>> dir(a8e)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a8e', 'main', 'sys', 'urllib2']
>>> a8e.a8e('hello')
'h3o'
>>>
The filesystem layout looks something like this:
a8e/ |-- a8e | |-- a8e.py | `-- __init__.py |-- setup.cfg `-- setup.py
(and the setup.cfg is pretty optional)
It is easier to start a new project from an example.
http://k0s.org/hg/webob_view/Installing webob_view:
# create a virtualenv if you haven't hg clone http://k0s.org/hg/webob_view/ cd webob_view python setup.py develop
Listing available PasteScript templates:
(stage)> paster create --list-templates Available templates: basic_package: A basic setuptools-enabled package command_script: pastescript template for creating command line applications console_script: pastescript template for creating command line applications genshi_view: a simple view with webob + genshi paste_deploy: A web application deployed through paste.deploy webob_view: a simple view with webob
Creating a new project with webob_view :
(stage)> paster create -t webob_view hello
Selected and implied templates:
webob-view#webob_view a simple view with webob
Variables:
egg: hello
package: hello
project: hello
Enter description (One-line description of the package) ['']: a
description
Enter author (Author name) ['']: Jeff Hammel
Enter author_email (Author email) ['']: jhammel@mozilla.com
Enter url (URL of homepage) ['']: http://k0s.org/
Enter port (port to serve paste) ['']: 7654
Creating template webob_view
Creating directory ./hello
Recursing into +package+
Creating ./hello/hello/
Copying __init__.py to ./hello/hello/__init__.py
Copying dispatcher.py to ./hello/hello/dispatcher.py
Copying factory.py_tmpl to ./hello/hello/factory.py
Copying handlers.py to ./hello/hello/handlers.py
Copying +package+.ini_tmpl to ./hello/hello.ini
Copying README.txt_tmpl to ./hello/README.txt
Copying setup.py_tmpl to ./hello/setup.py
Running /home/jhammel/stage/bin/python setup.py egg_info
Serving the created project:
cd hello python setup.py develop (stage)> paster serve hello.ini Starting server in PID 2050. serving on 0.0.0.0:7654 view at http://127.0.0.1:7654
Viewing the page:
> curl http://127.0.0.1:7654 <html><body><form method="post">Hello, <input type="text" name="name" value="world"/></form></body></html>
Or use your browser!
When you render the webob_view template, what do you get out of it?
Being a template, you are allowed -- nay, encouraged! -- to alter any of the resultant code (pylons has a similar philosophy)
Why web templates?
Python web templates:
Templates are typically used by passing in variables. In python, this is usually a dict.
Most template flavors allow simple display-oriented logic (django's does not)
If you don't use templates, your code ends up looking like this:
def application(environ, start_response):
"""a simple application to alphabetize words"""
request = Request(environ)
words = sorted(request.GET.keys())
variables = { 'title': request.GET.get('title', 'Hello World'),
'body': '<li>' + '</li><li>'.join(words) + '</li>'
}
template = """<html><head><title>%(title)s</title><body><h1>%(title)s</h1><div><ul>%(body)s</ul></div></body></html>""" % variables
response = Response(content_type='text/html',
template)
return response(environ, start_response)
Genshi is an XML and text templating language that focuses on robustness and streams
The previous example:
from genshi.template import TemplateLoader
loader = TemplateLoader('/path/to/directory')
def application(environ, start_response):
"""a simple application to alphabetize words"""
request = Request(environ)
words = sorted(request.GET.keys())
variables = { 'title': request.GET.get('title', 'Hello World'),
'words': words
}
template = self.loader.load('alphabetize.html')
content = template.genereate(**variables).render()
response = Response(content_type='text/html',
content)
return response(environ, start_response)
The template:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/">
<head>
<title>${title}</title>
</head>
<body>
<h1>${title}</h1>
<div>
<ul>
<li py:for="word in words">${word}</li>
</ul>
</div>
</body>
</html>
alphabetize.html
Remember the crazy URL that made the page black and blink weird?
http://k0s.org/mozilla/craft/?show_header=Accept,Host,Unicorn&blink=true&black#end
This is all done with a genshi template. The WebOb Request object is passed in. From there, the template does the rest.
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<head>
<title>Beginning Python Web Services</title>
<link rel="stylesheet" type="text/css" href="/css/professional.css" title="Default"/>
<style type="text/css">
body {
width: 800px;
alignment: center;
margin-left: auto;
margin-right: auto;
}
</style>
</head>
<body py:attrs="'black' in request.GET and {'bgcolor': 'black'} or {}">
<xi:include href="site-nav.html"/>
<h1>
<a href="http://p2pu.org/webcraft/beginning-python-webservices"
title="p2pu course page">
<blink py:strip="request.GET.get('blink') != 'true'">
Beginning Python Web Services
</blink>
</a>
</h1>
<h2>Material</h2>
<ul id="listing">
<li><a href="introduction.html">Introduction</a></li>
<li><a href="http.html">HTTP</a></li>
<li><a href="project.html">Project</a></li>
<li><a href="python.html">Python and the Web</a></li>
<li><a href="templates.html">Templates</a></li>
<li><a href="middleware.html">Middleware</a></li>
<li><a href="deployment.html">Deployment</a></li>
<li><a href="architecture.html">Web Service and Web Site Architecture</a></li>
<li><a href="email.html">Email Services</a></li>
</ul>
<h2><a href="questions.html">Homework questions</a></h2>
<div py:if="'show_header' in request.GET"
py:with="headers = request.GET['show_header'] and request.GET['show_header'].split(',') or sorted(request.headers.keys())">
<div py:for="header in headers">
<a href="http://www.google.com/search?q=${header}+HTTP+header">
<i py:strip="header in request.headers">
${'%s: %s' % (header, request.headers.get(header, 'The request did not contain header "%s"' % header))}
</i>
</a>
</div>
</div>
<a name="end">The End</a>
</body>
</html>
genshi_view extends webob_view with genshi templates
Other features over webob_view:
You may not need these for every project, but they're easy to delete and they might be useful
Yes, this is a system of (web) templates in a (file) template
(stage)> paster create -t genshi_view SimpleWiki
Selected and implied templates:
genshi-view#genshi_view a simple view with webob + genshi
Variables:
egg: SimpleWiki
package: simplewiki
project: SimpleWiki
Enter description (One-line description of the package) ['']: an example wiki with genshi
Enter author (Author name) ['']: Jeff Hammel
Enter author_email (Author email) ['']: jhammel@mozilla.com
Enter url (URL of homepage) ['']: http://k0s.org/mozilla/craft/
Enter port (port to serve paste) ['']: 12345
Creating template genshi_view
Creating directory ./SimpleWiki
Recursing into +package+
Creating ./SimpleWiki/simplewiki/
Copying __init__.py to ./SimpleWiki/simplewiki/__init__.py
Copying dispatcher.py to ./SimpleWiki/simplewiki/dispatcher.py
Copying factory.py_tmpl to ./SimpleWiki/simplewiki/factory.py
Copying handlers.py to ./SimpleWiki/simplewiki/handlers.py
Recursing into static
Creating ./SimpleWiki/simplewiki/static/
Copying jquery.js to ./SimpleWiki/simplewiki/static/jquery.js
Recursing into templates
Creating ./SimpleWiki/simplewiki/templates/
Copying index.html to ./SimpleWiki/simplewiki/templates/index.html
Copying navigation.html to ./SimpleWiki/simplewiki/templates/navigation.html
Copying +package+.ini_tmpl to ./SimpleWiki/simplewiki.ini
Copying README.txt_tmpl to ./SimpleWiki/README.txt
Copying setup.py_tmpl to ./SimpleWiki/setup.py
Running /home/jhammel/stage/bin/python setup.py egg_info
(stage)> cd SimpleWiki/
(stage)> python setup.py develop
running develop
running egg_info
writing requirements to SimpleWiki.egg-info/requires.txt
writing SimpleWiki.egg-info/PKG-INFO
writing top-level names to SimpleWiki.egg-info/top_level.txt
writing dependency_links to SimpleWiki.egg-info/dependency_links.txt
writing entry points to SimpleWiki.egg-info/entry_points.txt
writing requirements to SimpleWiki.egg-info/requires.txt
writing SimpleWiki.egg-info/PKG-INFO
writing top-level names to SimpleWiki.egg-info/top_level.txt
writing dependency_links to SimpleWiki.egg-info/dependency_links.txt
writing entry points to SimpleWiki.egg-info/entry_points.txt
reading manifest file 'SimpleWiki.egg-info/SOURCES.txt'
writing manifest file 'SimpleWiki.egg-info/SOURCES.txt'
running build_ext
Creating /home/jhammel/stage/lib/python2.6/site-packages/SimpleWiki.egg-link (link to .)
Adding SimpleWiki 0.0 to easy-install.pth file
Installed /home/jhammel/stage/src/SimpleWiki
Processing dependencies for SimpleWiki==0.0
Searching for Genshi==0.6
Best match: Genshi 0.6
Processing Genshi-0.6-py2.6.egg
Genshi 0.6 is already the active version in easy-install.pth
Using /home/jhammel/stage/lib/python2.6/site-packages/Genshi-0.6-py2.6.egg
Searching for PasteScript==1.7.3
Best match: PasteScript 1.7.3
Processing PasteScript-1.7.3-py2.6.egg
PasteScript 1.7.3 is already the active version in easy-install.pth
Installing paster script to /home/jhammel/stage/bin
Installing paster script to /home/jhammel/stage/bin
Using /home/jhammel/stage/lib/python2.6/site-packages/PasteScript-1.7.3-py2.6.egg
Searching for Paste==1.7.3.1
Best match: Paste 1.7.3.1
Processing Paste-1.7.3.1-py2.6.egg
Paste 1.7.3.1 is already the active version in easy-install.pth
Using /home/jhammel/stage/lib/python2.6/site-packages/Paste-1.7.3.1-py2.6.egg
Searching for WebOb==0.9.8
Best match: WebOb 0.9.8
Processing WebOb-0.9.8-py2.6.egg
WebOb 0.9.8 is already the active version in easy-install.pth
Using /home/jhammel/stage/lib/python2.6/site-packages/WebOb-0.9.8-py2.6.egg
Searching for PasteDeploy==1.3.3
Best match: PasteDeploy 1.3.3
Processing PasteDeploy-1.3.3-py2.6.egg
PasteDeploy 1.3.3 is already the active version in easy-install.pth
Using /home/jhammel/stage/lib/python2.6/site-packages/PasteDeploy-1.3.3-py2.6.egg
Finished processing dependencies for SimpleWiki==0.0
(stage)>
diff --git a/example/hello.html b/example/hello.html
new file mode 100644
--- /dev/null
+++ b/example/hello.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Hello, world!</title>
+</head>
+<body>
+Hello, world!
+</body>
+</html>
diff --git a/simplewiki.ini b/simplewiki.ini
--- a/simplewiki.ini
+++ b/simplewiki.ini
@@ -14,9 +14,9 @@ port = 12345
[composite:main]
use = egg:Paste#urlmap
/ = SimpleWiki
set debug = false
[app:SimpleWiki]
paste.app_factory = simplewiki.factory:factory
-SimpleWiki.name = world
\ No newline at end of file
+SimpleWiki.directory = %(here)s/example
\ No newline at end of file
diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,43 +1,41 @@
"""
request dispatcher:
data persisting across requests should go here
"""
import os
-from handlers import Index
+from handlers import GenshiRenderer
from genshi.template import TemplateLoader
from paste.fileapp import FileApp
from pkg_resources import resource_filename
from webob import Request, Response, exc
class Dispatcher(object):
### class level variables
defaults = { 'auto_reload': 'False',
'template_dirs': '',
- 'app': None,
- 'name': 'anonymous' }
+ 'name': 'anonymous',
+ 'directory': None }
def __init__(self, **kw):
# set instance parameters from kw and defaults
for key in self.defaults:
setattr(self, key, kw.get(key, self.defaults[key]))
self.auto_reload = self.auto_reload.lower() == 'true'
+ assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
+
# request handlers
- self.handlers = [ Index ]
-
- # endpoint app if used as middleware
- if self.app:
- assert hasattr(self.app, '__call__')
+ self.handlers = [ GenshiRenderer ]
# template loader
self.template_dirs = self.template_dirs.split()
self.template_dirs.append(resource_filename(__name__, 'templates'))
self.loader = TemplateLoader(self.template_dirs,
auto_reload=self.auto_reload)
def __call__(self, environ, start_response):
@@ -52,18 +50,16 @@ class Dispatcher(object):
request.environ['path'] = path
# match the request to a handler
for h in self.handlers:
handler = h.match(self, request)
if handler is not None:
break
else:
- if self.app:
- return self.app(environ, start_response)
handler = exc.HTTPNotFound
# add navigation links to handler [example]
if hasattr(handler, 'data'):
handler.data.setdefault('links', [])
for h in self.handlers:
handler.data['links'].append((handler.link(h.handler_path),
h.__name__))
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -1,13 +1,14 @@
"""
request handlers:
these are instantiated for every request, then called
"""
+import os
from urlparse import urlparse
from webob import Response, exc
class HandlerMatchException(Exception):
"""the handler doesn't match the request"""
class Handler(object):
@@ -44,38 +45,47 @@ class Handler(object):
else:
application_url = [ self.application_path ]
path = application_url + path
return '/'.join(path)
def redirect(self, location):
raise exc.HTTPSeeOther(location=location)
-class GenshiHandler(Handler):
+class GenshiRenderer(Handler):
+
+ @classmethod
+ def match(cls, app, request):
+
+ # check the method
+ if request.method not in cls.methods:
+ return None
+
+ # check the path
+ path = request.environ['path']
+ if not path:
+ return None
+ if not path[-1].endswith('.html'):
+ return None
+
+
+ try:
+ return cls(app, request)
+ except HandlerMatchException:
+ return None
def __init__(self, app, request):
Handler.__init__(self, app, request)
+ self.template = os.path.join(app.directory, *request.environ['path'])
+ if not os.path.exists(self.template):
+ raise HandlerMatchException
self.data = { 'request': request,
'link': self.link }
def __call__(self):
return getattr(self, self.request.method.title())()
def Get(self):
# needs to have self.template set
template = self.app.loader.load(self.template)
return Response(content_type='text/html',
body=template.generate(**self.data).render('html'))
-class Index(GenshiHandler):
- template = 'index.html'
- methods=set(['GET', 'POST'])
-
- def __init__(self, app, request):
- GenshiHandler.__init__(self, app, request)
-
- def Get(self):
- self.data['name'] = self.request.remote_user or self.app.name
- return GenshiHandler.Get(self)
-
- def Post(self):
- self.app.name = self.request.POST.get('name', self.app.name)
- self.redirect(self.link(self.handler_path))
changes to add renderer
diff format
diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
"""
request dispatcher:
data persisting across requests should go here
"""
import os
-from handlers import GenshiRenderer
+from handlers import GenshiRenderer, Index
from genshi.template import TemplateLoader
from paste.fileapp import FileApp
from pkg_resources import resource_filename
from webob import Request, Response, exc
class Dispatcher(object):
@@ -25,17 +25,17 @@ class Dispatcher(object):
# set instance parameters from kw and defaults
for key in self.defaults:
setattr(self, key, kw.get(key, self.defaults[key]))
self.auto_reload = self.auto_reload.lower() == 'true'
assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
# request handlers
- self.handlers = [ GenshiRenderer ]
+ self.handlers = [ GenshiRenderer, Index ]
# template loader
self.template_dirs = self.template_dirs.split()
self.template_dirs.append(resource_filename(__name__, 'templates'))
self.loader = TemplateLoader(self.template_dirs,
auto_reload=self.auto_reload)
def __call__(self, environ, start_response):
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -79,13 +79,40 @@ class GenshiRenderer(Handler):
raise HandlerMatchException
self.data = { 'request': request,
'link': self.link }
def __call__(self):
return getattr(self, self.request.method.title())()
def Get(self):
- # needs to have self.template set
template = self.app.loader.load(self.template)
return Response(content_type='text/html',
body=template.generate(**self.data).render('html'))
+
+class Index(Handler):
+
+ template = 'index.html'
+
+ def __init__(self, app, request):
+ Handler.__init__(self, app, request)
+ self.directory = os.path.join(app.directory, *request.environ['path'])
+ if not os.path.isdir(self.directory):
+ raise HandlerMatchException
+ path = request.environ['path']
+ files = []
+ files = os.listdir(self.directory)
+ self.data = { 'request': request,
+ 'link': self.link,
+ 'directory': '/' + '/'.join(path),
+ 'files': files }
+
+ def __call__(self):
+ return getattr(self, self.request.method.title())()
+
+ def Get(self):
+ if not self.request.path_info.endswith('/'):
+ self.redirect(self.request.path_info + '/')
+ template = self.app.loader.load(self.template)
+ return Response(content_type='text/html',
+ body=template.generate(**self.data).render('html'))
+
diff --git a/simplewiki/templates/index.html b/simplewiki/templates/index.html
--- a/simplewiki/templates/index.html
+++ b/simplewiki/templates/index.html
@@ -1,22 +1,16 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<head>
-<title>Hello world</title>
-<script src="${link('jquery.js')}"></script>
-<script type="text/javascript">
-$(document).ready(function(){
-$(".text-input").click(function(){
-$(this).replaceWith('<form method="post"><input type="text" name="name" value="${name}"/><input type="submit" value="Go!"/></form>');
-});
-});
-</script>
+<title>${directory}</title>
</head>
<body>
-<xi:include href="navigation.html" />
-Hello <span class="text-input">${name}</span>!
+<ul>
+ <li py:if="request.path_info != '/'"><a href="..">..</a></li>
+ <li py:for="f in files"><a href="${f}">${f}</a></li>
+</ul>
</body>
</html>
diff --git a/simplewiki/templates/navigation.html b/simplewiki/templates/navigation.html
deleted file mode 100644
--- a/simplewiki/templates/navigation.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html
- PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- py:strip="True">
-
- <!-- nav bar -->
- <div class="site-nav">
- <ul>
- <li py:for="link, name in links">
- <a href="${link}">${name}</a>
- </li>
- </ul>
- </div>
-
-</html>
changes to add index handler
diff --git a/example/avatar.jpg b/example/avatar.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..94b272a84793e8c46e7b757406e6c1e87153119b
GIT binary patch
literal 3148
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
diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
"""
request dispatcher:
data persisting across requests should go here
"""
import os
-from handlers import GenshiRenderer, Index
+from handlers import GenshiRenderer, Index, Post
from genshi.template import TemplateLoader
from paste.fileapp import FileApp
from pkg_resources import resource_filename
from webob import Request, Response, exc
class Dispatcher(object):
@@ -25,17 +25,17 @@ class Dispatcher(object):
# set instance parameters from kw and defaults
for key in self.defaults:
setattr(self, key, kw.get(key, self.defaults[key]))
self.auto_reload = self.auto_reload.lower() == 'true'
assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
# request handlers
- self.handlers = [ GenshiRenderer, Index ]
+ self.handlers = [ Post, GenshiRenderer, Index ]
# template loader
self.template_dirs = self.template_dirs.split()
self.template_dirs.append(resource_filename(__name__, 'templates'))
self.loader = TemplateLoader(self.template_dirs,
auto_reload=self.auto_reload)
def __call__(self, environ, start_response):
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -111,8 +111,34 @@ class Index(Handler):
def Get(self):
if not self.request.path_info.endswith('/'):
self.redirect(self.request.path_info + '/')
template = self.app.loader.load(self.template)
return Response(content_type='text/html',
body=template.generate(**self.data).render('html'))
+class Post(Handler):
+ methods = set(['POST']) # methods to listen to
+
+ def __init__(self, app, request):
+ Handler.__init__(self, app, request)
+ if 'file' not in request.POST:
+ raise HandlerMatchException
+ self.file = self.request.POST['file']
+ if not getattr(self.file, 'filename', None):
+ raise HandlerMatchException
+ self.location = request.path_info.rstrip('/')
+ path = os.path.join(self.app.directory, *self.request.environ['path'])
+ if os.path.isdir(path):
+ self.directory = path
+ self.filename = os.path.join(self.directory, self.file.filename)
+ self.location += '/' + self.file.filename
+ else:
+ self.directory = os.path.dirname(path)
+ self.filename = path
+
+ f = file(self.filename, 'wb')
+ f.write(self.file.file.read())
+ f.close()
+
+ def __call__(self):
+ self.redirect(self.location)
diff --git a/simplewiki/templates/index.html b/simplewiki/templates/index.html
--- a/simplewiki/templates/index.html
+++ b/simplewiki/templates/index.html
@@ -7,10 +7,16 @@
<head>
<title>${directory}</title>
</head>
<body>
<ul>
<li py:if="request.path_info != '/'"><a href="..">..</a></li>
<li py:for="f in files"><a href="${f}">${f}</a></li>
</ul>
+<form method="post" enctype="multipart/form-data">
+<p>Upload a file:</p>
+<input type="file" name="file"/>
+<input type="submit"/>
+</form>
+
</body>
</html>
changes to add POST handler
Add a FileApp handler for other content
diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
"""
request dispatcher:
data persisting across requests should go here
"""
import os
-from handlers import GenshiRenderer, Index, Post
+from handlers import GenshiRenderer, Index, Post, FileServer
from genshi.template import TemplateLoader
from paste.fileapp import FileApp
from pkg_resources import resource_filename
from webob import Request, Response, exc
class Dispatcher(object):
@@ -25,17 +25,17 @@ class Dispatcher(object):
# set instance parameters from kw and defaults
for key in self.defaults:
setattr(self, key, kw.get(key, self.defaults[key]))
self.auto_reload = self.auto_reload.lower() == 'true'
assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
# request handlers
- self.handlers = [ Post, GenshiRenderer, Index ]
+ self.handlers = [ Post, GenshiRenderer, Index, FileServer ]
# template loader
self.template_dirs = self.template_dirs.split()
self.template_dirs.append(resource_filename(__name__, 'templates'))
self.loader = TemplateLoader(self.template_dirs,
auto_reload=self.auto_reload)
def __call__(self, environ, start_response):
@@ -52,18 +52,11 @@ class Dispatcher(object):
# match the request to a handler
for h in self.handlers:
handler = h.match(self, request)
if handler is not None:
break
else:
handler = exc.HTTPNotFound
- # add navigation links to handler [example]
- if hasattr(handler, 'data'):
- handler.data.setdefault('links', [])
- for h in self.handlers:
- handler.data['links'].append((handler.link(h.handler_path),
- h.__name__))
-
# get response
res = handler()
return res(environ, start_response)
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -1,14 +1,15 @@
"""
request handlers:
these are instantiated for every request, then called
"""
import os
+from paste.fileapp import FileApp
from urlparse import urlparse
from webob import Response, exc
class HandlerMatchException(Exception):
"""the handler doesn't match the request"""
class Handler(object):
@@ -137,8 +138,21 @@ class Post(Handler):
self.filename = path
f = file(self.filename, 'wb')
f.write(self.file.file.read())
f.close()
def __call__(self):
self.redirect(self.location)
+
+class FileServer(Handler):
+ methods = set(['GET']) # methods to listen to
+
+ def __init__(self, app, request):
+ Handler.__init__(self, app, request)
+ self.file = os.path.join(self.app.directory, *request.environ['path'])
+ if not os.path.exists(self.file):
+ raise HandlerMatchException
+
+ def __call__(self):
+ return FileApp(self.file)
+
changes to add file server
diff --git a/example/hello.html b/example/hello.html
--- a/example/hello.html
+++ b/example/hello.html
@@ -1,8 +1,8 @@
-<html>
-<head>
-<title>Hello, world!</title>
-</head>
-<body>
-Hello, world!
-</body>
-</html>
+<html>
+<head>
+<title>Hello, world!</title>
+</head>
+<body>
+Hello, worldz!
+</body>
+</html>
diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
"""
request dispatcher:
data persisting across requests should go here
"""
import os
-from handlers import GenshiRenderer, Index, Post, FileServer
+from handlers import GenshiRenderer, Index, Post, FileServer, EditView
from genshi.template import TemplateLoader
from paste.fileapp import FileApp
from pkg_resources import resource_filename
from webob import Request, Response, exc
class Dispatcher(object):
@@ -25,17 +25,17 @@ class Dispatcher(object):
# set instance parameters from kw and defaults
for key in self.defaults:
setattr(self, key, kw.get(key, self.defaults[key]))
self.auto_reload = self.auto_reload.lower() == 'true'
assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
# request handlers
- self.handlers = [ Post, GenshiRenderer, Index, FileServer ]
+ self.handlers = [ Post, EditView, GenshiRenderer, Index, FileServer ]
# template loader
self.template_dirs = self.template_dirs.split()
self.template_dirs.append(resource_filename(__name__, 'templates'))
self.loader = TemplateLoader(self.template_dirs,
auto_reload=self.auto_reload)
def __call__(self, environ, start_response):
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -120,30 +120,34 @@ class Index(Handler):
class Post(Handler):
methods = set(['POST']) # methods to listen to
def __init__(self, app, request):
Handler.__init__(self, app, request)
if 'file' not in request.POST:
raise HandlerMatchException
self.file = self.request.POST['file']
- if not getattr(self.file, 'filename', None):
- raise HandlerMatchException
+ filename = getattr(self.file, 'filename', '')
+ if filename:
+ content = self.file.file.read()
+ else:
+ content = self.file
+
self.location = request.path_info.rstrip('/')
path = os.path.join(self.app.directory, *self.request.environ['path'])
if os.path.isdir(path):
self.directory = path
- self.filename = os.path.join(self.directory, self.file.filename)
- self.location += '/' + self.file.filename
+ self.filename = os.path.join(self.directory, filename)
+ self.location += '/' + filename
else:
self.directory = os.path.dirname(path)
self.filename = path
f = file(self.filename, 'wb')
- f.write(self.file.file.read())
+ f.write(content)
f.close()
def __call__(self):
self.redirect(self.location)
class FileServer(Handler):
methods = set(['GET']) # methods to listen to
@@ -151,8 +155,34 @@ class FileServer(Handler):
Handler.__init__(self, app, request)
self.file = os.path.join(self.app.directory, *request.environ['path'])
if not os.path.exists(self.file):
raise HandlerMatchException
def __call__(self):
return FileApp(self.file)
+class EditView(Handler):
+ methods = set(['GET'])
+ template = 'edit.html'
+
+ def __init__(self, app, request):
+ if 'edit' not in request.GET:
+ raise HandlerMatchException
+
+ Handler.__init__(self, app, request)
+
+ self.file = os.path.join(self.app.directory, *request.environ['path'])
+ if not os.path.exists(self.file):
+ raise HandlerMatchException
+ self.data = { 'request': request,
+ 'link': self.link,
+ 'file': '/' + '/'.join(request.environ['path']),
+ 'content': file(self.file).read()
+ }
+
+ def __call__(self):
+ return getattr(self, self.request.method.title())()
+
+ def Get(self):
+ template = self.app.loader.load(self.template)
+ return Response(content_type='text/html',
+ body=template.generate(**self.data).render('html'))
changes to add edit view
The code lives here: /hg/SimpleWiki
Defects:
Features:
The point is, its not hard to build an app this way. SimpleWiki is an example application an something to build on, not a comprehensive solution as-is.
See also cousin decoupage
In order to make a meaningful web service, it must be independent of other web services
Note that each of the handlers is a distinct web service. They don't talk to each other and have standalone functionality.
How you would do this in pylons:
How we could improve this in SimpleWiki: