Initial commit.

This commit is contained in:
Jonathan LaCour 2010-09-28 13:08:30 -04:00
commit 5c32201386
20 changed files with 365 additions and 0 deletions

0
README Normal file
View File

10
pecan.egg-info/PKG-INFO Normal file
View File

@ -0,0 +1,10 @@
Metadata-Version: 1.0
Name: pecan
Version: 0.1dev
Summary: A WSGI object-dispatching web framework, in the spirit of TurboGears, only much much smaller, with many fewer dependancies.
Home-page: http://code.google.com/p/pecan
Author: Jonathan LaCour
Author-email: jonathan@cleverdevil.org
License: BSD
Description: UNKNOWN
Platform: UNKNOWN

View File

@ -0,0 +1,14 @@
setup.cfg
setup.py
pecan/__init__.py
pecan/decorators.py
pecan/jsonify.py
pecan/pecan.py
pecan/templating.py
pecan.egg-info/PKG-INFO
pecan.egg-info/SOURCES.txt
pecan.egg-info/dependency_links.txt
pecan.egg-info/entry_points.txt
pecan.egg-info/requires.txt
pecan.egg-info/top_level.txt
pecan.egg-info/zip-safe

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,3 @@
# -*- Entry points: -*-

View File

@ -0,0 +1,6 @@
WebOb >= 0.9.8
simplejson >= 2.0.9
simplegeneric >= 0.7
Genshi >= 0.6
Kajiki >= 0.2.2
Mako >= 0.3

View File

@ -0,0 +1 @@
pecan

1
pecan.egg-info/zip-safe Normal file
View File

@ -0,0 +1 @@

2
pecan/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from pecan import Pecan, request, override_template
from decorators import expose

BIN
pecan/__init__.pyc Normal file

Binary file not shown.

9
pecan/decorators.py Normal file
View File

@ -0,0 +1,9 @@
def expose(template=None, content_type='text/html'):
if template == 'json': content_type = 'application/json'
def decorate(f):
f.exposed = True
if not hasattr(f, 'pecan'): f.pecan = {}
f.pecan.setdefault('template', []).append(template)
f.pecan.setdefault('content_types', {})[content_type] = template
return f
return decorate

BIN
pecan/decorators.pyc Normal file

Binary file not shown.

65
pecan/jsonify.py Normal file
View File

@ -0,0 +1,65 @@
from simplejson import JSONEncoder, dumps
from datetime import datetime, date
from decimal import Decimal
from webob.multidict import MultiDict
from simplegeneric import generic
#
# exceptions
#
class JsonEncodeError(Exception):
pass
#
# encoders
#
class BaseEncoder(JSONEncoder):
def is_saobject(self, obj):
return hasattr(obj, '_sa_class_manager')
def jsonify(self, obj):
return dumps(self.encode(obj))
def encode(self, obj):
if hasattr(obj, '__json__') and callable(obj.__json__):
return obj.__json__()
elif isinstance(obj, (date, datetime)):
return str(obj)
elif isinstance(obj, Decimal):
return float(obj)
elif self.is_saobject(obj):
props = {}
for key in obj.__dict__:
if not key.startswith('_sa_'):
props[key] = getattr(obj, key)
return props
elif isinstance(obj, MultiDict):
return obj.mixed()
else:
try:
from sqlalchemy.engine.base import ResultProxy, RowProxy
if isinstance(obj, ResultProxy):
return dict(rows=list(obj), count=obj.rowcount)
elif isinstance(obj, RowProxy):
return dict(rows=dict(obj), count=1)
except:
pass
return obj
#
# generic function support
#
encoder = BaseEncoder()
@generic
def jsonify(obj):
return encoder.encode(obj)
def encode(obj):
return dumps(jsonify(obj))

BIN
pecan/jsonify.pyc Normal file

Binary file not shown.

137
pecan/pecan.py Normal file
View File

@ -0,0 +1,137 @@
from templating import renderers
from webob import Request, Response, exc
from threading import local
import string
state = local()
class RequestWrapper(object):
def __getattr__(self, attr):
return getattr(state.request, attr)
def __setattr__(self, attr, value):
return setattr(state.request, attr, value)
request = RequestWrapper()
def override_template(template):
request.override_template = template
class Pecan(object):
def __init__(self, root, renderers=renderers, default_renderer='genshi'):
self.root = root
self.renderers = renderers
self.default_renderer = default_renderer
self.translate = string.maketrans(
string.punctuation,
'_' * len(string.punctuation)
)
def get_content_type(self, format):
return {
'html' : 'text/html',
'xhtml' : 'text/html',
'json' : 'application/json'
}.get(format, 'text/html')
def route(self, node, path):
curpath = ""
nodeconf = {}
object_trail = [['root', self.root, nodeconf, curpath]]
names = [x for x in path.strip('/').split('/') if x] + ['index']
iternames = names[:]
while iternames:
name = iternames[0]
objname = name.translate(self.translate)
nodeconf = {}
subnode = getattr(node, objname, None)
name = iternames.pop(0)
node = subnode
curpath = "/".join((curpath, name))
object_trail.append([name, node, nodeconf, curpath])
# try successive objects (reverse order)
num_candidates = len(object_trail) - 1
for i in range(num_candidates, -1, -1):
name, candidate, nodeconf, curpath = object_trail[i]
if candidate is None:
continue
# try a "_route" method on the current leaf.
if hasattr(candidate, "_route"):
processed_path = object_trail[i][-1]
unprocessed_path = object_trail[-1][-1].replace(processed_path, '')
return candidate._route(unprocessed_path)
# try a "_lookup" method on the current leaf.
if hasattr(candidate, "_lookup"):
lookup = candidate._lookup(object_trail[i+1][0])
processed_path = object_trail[i+1][-1]
unprocessed_path = object_trail[-2][-1].replace(processed_path, '')
return self.route(lookup, unprocessed_path)
# try a "_default" method on the current leaf.
if hasattr(candidate, "_default"):
defhandler = candidate._default
if getattr(defhandler, 'exposed', False):
object_trail.insert(i+1, ["_default", defhandler, {}, curpath])
return defhandler
# try the current leaf.
if getattr(candidate, 'exposed', False):
return candidate
# we didn't find anything
return None
def __call__(self, environ, start_response):
# create the request object
state.request = Request(environ)
# lookup the controller
path = state.request.path
content_type = 'text/html'
if '.' in path.split('/')[-1]:
path, format = path.split('.')
content_type = self.get_content_type(format)
controller = self.route(self.root, path)
# if we didn't find a controller, issue a 404
if controller is None:
response = Response()
response.status = 404
return response(environ, start_response)
# get the result from the controller
result = controller(**dict(state.request.str_params))
# pull the template out based upon content type
template = controller.pecan.get('content_types', {}).get(content_type)
# handle template overrides
template = getattr(request, 'override_template', template)
if template:
renderer = self.renderers[self.default_renderer]
if template == 'json':
renderer = self.renderers['json']
elif len(self.renderers) > 1 and ':' in template:
renderer = self.renderers.get(template.split(':')[0], self.renderers.values()[0])
template = template.split(':')[1]
result = renderer.render(template, result)
content_type = renderer.content_type
response = Response(result)
if content_type:
response.content_type = content_type
del state.request
return response(environ, start_response)

BIN
pecan/pecan.pyc Normal file

Binary file not shown.

82
pecan/templating.py Normal file
View File

@ -0,0 +1,82 @@
renderers = {}
#
# JSON rendering engine
#
class JsonRenderer(object):
content_type = 'application/json'
def render(self, template_path, namespace):
from jsonify import encode
return encode(namespace)
renderers['json'] = JsonRenderer()
#
# Genshi rendering engine
#
try:
from genshi.template import TemplateLoader
class GenshiRenderer(object):
content_type = 'text/html'
def __init__(self):
self.loader = TemplateLoader(['templates'], auto_reload=True)
def render(self, template_path, namespace):
tmpl = self.loader.load(template_path)
stream = tmpl.generate(**namespace)
return stream.render('html')
except ImportError:
pass
else:
renderers['genshi'] = GenshiRenderer()
#
# Mako rendering engine
#
try:
from mako.lookup import TemplateLookup
class MakoRenderer(object):
content_type = 'text/html'
def __init__(self):
self.loader = TemplateLookup(directories=['templates'])
def render(self, template_path, namespace):
tmpl = self.loader.get_template(template_path)
return tmpl.render(**namespace)
except ImportError:
pass
else:
renderers['mako'] = MakoRenderer()
#
# Kajiki rendering engine
#
try:
from kajiki.loader import FileLoader
class KajikiRenderer(object):
content_type = 'text/html'
def __init__(self):
self.loader = FileLoader('templates', reload=True)
def render(self, template_path, namespace):
Template = self.loader.import_(template_path)
stream = Template(namespace)
return stream.render()
except ImportError:
pass
else:
renderers['kajiki'] = KajikiRenderer()

BIN
pecan/templating.pyc Normal file

Binary file not shown.

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[egg_info]
tag_build = dev
tag_svn_revision = true

31
setup.py Normal file
View File

@ -0,0 +1,31 @@
from setuptools import setup, find_packages
import sys, os
version = '0.1'
setup(
name = 'pecan',
version = version,
description = "A WSGI object-dispatching web framework, in the spirit of TurboGears, only much much smaller, with many fewer dependancies.",
long_description = None,
classifiers = [],
keywords = '',
author = 'Jonathan LaCour',
author_email = 'jonathan@cleverdevil.org',
url = 'http://code.google.com/p/pecan',
license = 'BSD',
packages = find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data = True,
zip_safe = True,
install_requires=[
"WebOb >= 0.9.8",
"simplejson >= 2.0.9",
"simplegeneric >= 0.7",
"Genshi >= 0.6",
"Kajiki >= 0.2.2",
"Mako >= 0.3"
],
entry_points = """
# -*- Entry points: -*-
""",
)