initial framework for crank, no tests yet
This commit is contained in:
commit
bfc9714eda
15
.hgignore
Normal file
15
.hgignore
Normal file
@ -0,0 +1,15 @@
|
||||
syntax:glob
|
||||
|
||||
.coverage
|
||||
*.pyc
|
||||
*.bak
|
||||
*.db
|
||||
*.sw?
|
||||
*.orig
|
||||
*.class
|
||||
build/*
|
||||
docs/.build/*
|
||||
dist/*
|
||||
.svn
|
||||
*.egg-info/*
|
||||
.noseids
|
0
crank/__init__.py
Normal file
0
crank/__init__.py
Normal file
160
crank/dispatcher.py
Normal file
160
crank/dispatcher.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""
|
||||
This is the main dispatcher module.
|
||||
|
||||
Dispatch works as follows:
|
||||
Start at the RootController, the root controller must
|
||||
have a _dispatch function, which defines how we move
|
||||
from object to object in the system.
|
||||
Continue following the dispatch mechanism for a given
|
||||
controller until you reach another controller with a
|
||||
_dispatch method defined. Use the new _dispatch
|
||||
method until anther controller with _dispatch defined
|
||||
or until the url has been traversed to entirety.
|
||||
|
||||
This module also contains the standard ObjectDispatch
|
||||
class which provides the ordinary TurboGears mechanism.
|
||||
|
||||
"""
|
||||
|
||||
from urllib import url2pathname
|
||||
from inspect import ismethod, isclass, getargspec
|
||||
|
||||
def remove_argspec_params_from_params(func, params, remainder):
|
||||
"""Remove parameters from the argument list that are
|
||||
not named parameters
|
||||
Returns: params, remainder"""
|
||||
|
||||
# figure out which of the vars in the argspec are required
|
||||
argspec = _get_argspec(func)
|
||||
argvars = argspec[0][1:]
|
||||
|
||||
# if there are no required variables, or the remainder is none, we
|
||||
# have nothing to do
|
||||
if not argvars or not remainder:
|
||||
return params, remainder
|
||||
|
||||
# this is a work around for a crappy api choice in getargspec
|
||||
argvals = argspec[3]
|
||||
if argvals is None:
|
||||
argvals = []
|
||||
|
||||
required_vars = argvars
|
||||
optional_vars = []
|
||||
if argvals:
|
||||
required_vars = argvars[:-len(argvals)]
|
||||
optional_vars = argvars[-len(argvals):]
|
||||
|
||||
# make a copy of the params so that we don't modify the existing one
|
||||
params=params.copy()
|
||||
|
||||
# replace the existing required variables with the values that come in
|
||||
# from params these could be the parameters that come off of validation.
|
||||
remainder = list(remainder)
|
||||
for i, var in enumerate(required_vars):
|
||||
if i < len(remainder):
|
||||
remainder[i] = params[var]
|
||||
elif params.get(var):
|
||||
remainder.append(params[var])
|
||||
if var in params:
|
||||
del params[var]
|
||||
|
||||
#remove the optional positional variables (remainder) from the named parameters
|
||||
# until we run out of remainder, that is, avoid creating duplicate parameters
|
||||
for i,(original,var) in enumerate(zip(remainder[len(required_vars):],optional_vars)):
|
||||
if var in params:
|
||||
remainder[ len(required_vars)+i ] = params[var]
|
||||
del params[var]
|
||||
|
||||
return params, tuple(remainder)
|
||||
|
||||
_cached_argspecs = {}
|
||||
def get_argspec(self, func):
|
||||
try:
|
||||
argspec = _cached_argspecs[func.im_func]
|
||||
except KeyError:
|
||||
argspec = _cached_argspecs[func.im_func] = getargspec(func)
|
||||
return argspec
|
||||
|
||||
def get_params_with_argspec(func, params, remainder):
|
||||
params = params.copy()
|
||||
argspec = get_argspec(func)
|
||||
argvars = argspec[0][1:]
|
||||
if argvars and enumerate(remainder):
|
||||
for i, var in enumerate(argvars):
|
||||
if i >= len(remainder):
|
||||
break
|
||||
params[var] = remainder[i]
|
||||
return params
|
||||
|
||||
def method_matches_args(self, method, state, remainder):
|
||||
"""
|
||||
This method matches the params from the request along with the remainder to the
|
||||
method's function signiture. If the two jive, it returns true.
|
||||
|
||||
It is very likely that this method would go into ObjectDispatch in the future.
|
||||
"""
|
||||
argspec = get_argspec(method)
|
||||
argvars = argspec[0][1:]
|
||||
argvals = argspec[3]
|
||||
|
||||
required_vars = argvars
|
||||
if argvals:
|
||||
required_vars = argvars[:-len(argvals)]
|
||||
else:
|
||||
argvals = []
|
||||
|
||||
#remove the appropriate remainder quotient
|
||||
if len(remainder)<len(required_vars):
|
||||
#pull the first few off with the remainder
|
||||
required_vars = required_vars[len(remainder):]
|
||||
else:
|
||||
#there is more of a remainder than there is non optional vars
|
||||
required_vars = []
|
||||
|
||||
#remove vars found in the params list
|
||||
params = state.params
|
||||
for var in required_vars[:]:
|
||||
if var in params:
|
||||
required_vars.pop(0)
|
||||
else:
|
||||
break;
|
||||
|
||||
var_in_params = 0
|
||||
for var in argvars:
|
||||
if var in params:
|
||||
var_in_params+=1
|
||||
|
||||
#make sure all of the non-optional-vars are there
|
||||
if not required_vars:
|
||||
var_args = argspec[0][1:]
|
||||
#there are more args in the remainder than are available in the argspec
|
||||
if len(var_args)<len(remainder) and not argspec[1]:
|
||||
return False
|
||||
defaults = argspec[3] or []
|
||||
var_args = var_args[len(remainder):-len(defaults)]
|
||||
for arg in var_args:
|
||||
if arg not in state.params:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
class Dispatcher(object):
|
||||
"""
|
||||
Extend this class to define your own mechanism for dispatch.
|
||||
"""
|
||||
|
||||
def _dispatch(self, state, remainder):
|
||||
"""override this to define how your controller should dispatch.
|
||||
returns: dispatcher, controller_path, remainder
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _setup_wsgiorg_routing_args(self, url_path, remainder, params):
|
||||
"""
|
||||
This is expected to be overridden by any subclass that wants to set
|
||||
the routing_args.
|
||||
"""
|
||||
|
||||
def _setup_wsgi_script_name(self, url_path, remainder, params):
|
||||
pass
|
||||
|
54
crank/dispatchstate.py
Normal file
54
crank/dispatchstate.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""
|
||||
This module implements the :class:`DispatchState` class
|
||||
"""
|
||||
from util import odict
|
||||
|
||||
class DispatchState(object):
|
||||
"""
|
||||
This class keeps around all the pertainent info for the state
|
||||
of the dispatch as it traverses through the tree. This allows
|
||||
us to attach things like routing args and to keep track of the
|
||||
path the controller takes along the system.
|
||||
"""
|
||||
def __init__(self, request, params=None):
|
||||
self.request = request
|
||||
self.url_path = request.path_info
|
||||
|
||||
if params:
|
||||
self.params = params
|
||||
else:
|
||||
self.params = request.params
|
||||
|
||||
self.controller_path = odict()
|
||||
self.routing_args = {}
|
||||
self.method = None
|
||||
self.remainder = None
|
||||
self.dispatcher = None
|
||||
self.params = params
|
||||
|
||||
def add_controller(self, location, controller):
|
||||
"""Add a controller object to the stack"""
|
||||
self.controller_path[location] = controller
|
||||
|
||||
def add_method(self, method, remainder):
|
||||
"""Add the final method that will be called in the _call method"""
|
||||
self.method = method
|
||||
self.remainder = remainder
|
||||
|
||||
def add_routing_args(self, current_path, remainder, fixed_args, var_args):
|
||||
"""
|
||||
Add the "intermediate" routing args for a given controller mounted
|
||||
at the current_path
|
||||
"""
|
||||
for i, arg in enumerate(fixed_args):
|
||||
if i >= len(remainder):
|
||||
break
|
||||
self.routing_args[arg] = remainder[i]
|
||||
remainder = remainder[i:]
|
||||
if var_args and remainder:
|
||||
self.routing_args[current_path] = remainder
|
||||
|
||||
@property
|
||||
def controller(self):
|
||||
"""returns the current controller"""
|
||||
return self.controller_path.getitem(-1)
|
179
crank/objectdispatcher.py
Normal file
179
crank/objectdispatcher.py
Normal file
@ -0,0 +1,179 @@
|
||||
"""
|
||||
This is the main dispatcher module.
|
||||
|
||||
Dispatch works as follows:
|
||||
Start at the RootController, the root controller must
|
||||
have a _dispatch function, which defines how we move
|
||||
from object to object in the system.
|
||||
Continue following the dispatch mechanism for a given
|
||||
controller until you reach another controller with a
|
||||
_dispatch method defined. Use the new _dispatch
|
||||
method until anther controller with _dispatch defined
|
||||
or until the url has been traversed to entirety.
|
||||
|
||||
This module also contains the standard ObjectDispatch
|
||||
class which provides the ordinary TurboGears mechanism.
|
||||
|
||||
"""
|
||||
|
||||
from dispatcher import get_argspec, method_matches_args, Dispatcher
|
||||
from webob.exc import HTTPNotFound
|
||||
|
||||
class ObjectDispatcher(Dispatcher):
|
||||
"""
|
||||
Object dispatch (also "object publishing") means that each portion of the
|
||||
URL becomes a lookup on an object. The next part of the URL applies to the
|
||||
next object, until you run out of URL. Processing starts on a "Root"
|
||||
object.
|
||||
|
||||
Thus, /foo/bar/baz become URL portion "foo", "bar", and "baz". The
|
||||
dispatch looks for the "foo" attribute on the Root URL, which returns
|
||||
another object. The "bar" attribute is looked for on the new object, which
|
||||
returns another object. The "baz" attribute is similarly looked for on
|
||||
this object.
|
||||
|
||||
Dispatch does not have to be directly on attribute lookup, objects can also
|
||||
have other methods to explain how to dispatch from them. The search ends
|
||||
when a decorated controller method is found.
|
||||
|
||||
The rules work as follows:
|
||||
|
||||
1) If the current object under consideration is a decorated controller
|
||||
method, the search is ended.
|
||||
|
||||
2) If the current object under consideration has a "default" method, keep a
|
||||
record of that method. If we fail in our search, and the most recent
|
||||
method recorded is a "default" method, then the search is ended with
|
||||
that method returned.
|
||||
|
||||
3) If the current object under consideration has a "lookup" method, keep a
|
||||
record of that method. If we fail in our search, and the most recent
|
||||
method recorded is a "lookup" method, then execute the "lookup" method,
|
||||
and start the search again on the return value of that method.
|
||||
|
||||
4) If the URL portion exists as an attribute on the object in question,
|
||||
start searching again on that attribute.
|
||||
|
||||
5) If we fail our search, try the most recent recorded methods as per 2 and
|
||||
3.
|
||||
"""
|
||||
def _find_first_exposed(self, controller, methods):
|
||||
for method in methods:
|
||||
if self._is_exposed(controller, method):
|
||||
return getattr(controller, method)
|
||||
|
||||
def _is_exposed(self, controller, name):
|
||||
"""Override this function to define how a controller method is
|
||||
determined to be exposed.
|
||||
|
||||
:Arguments:
|
||||
controller - controller with methods that may or may not be exposed.
|
||||
name - name of the method that is tested.
|
||||
|
||||
:Returns:
|
||||
True or None
|
||||
"""
|
||||
if hasattr(controller, name) and ismethod(getattr(controller, name)):
|
||||
return True
|
||||
|
||||
def _is_controller(self, controller, name):
|
||||
"""
|
||||
Override this function to define how an object is determined to be a
|
||||
controller.
|
||||
"""
|
||||
return hasattr(controller, name) and not ismethod(getattr(controller, name))
|
||||
|
||||
def _dispatch_controller(self, current_path, controller, state, remainder):
|
||||
"""
|
||||
Essentially, this method defines what to do when we move to the next
|
||||
layer in the url chain, if a new controller is needed.
|
||||
If the new controller has a _dispatch method, dispatch proceeds to
|
||||
the new controller's mechanism.
|
||||
|
||||
Also, this is the place where the controller is checked for
|
||||
controller-level security.
|
||||
"""
|
||||
#xxx: add logging?
|
||||
if hasattr(controller, '_dispatch'):
|
||||
if hasattr(controller, "im_self"):
|
||||
obj = controller.im_self
|
||||
else:
|
||||
obj = controller
|
||||
|
||||
if hasattr(obj, '_check_security'):
|
||||
obj._check_security()
|
||||
state.add_controller(current_path, controller)
|
||||
state.dispatcher = controller
|
||||
return controller._dispatch(state, remainder)
|
||||
state.add_controller(current_path, controller)
|
||||
return self._dispatch(state, remainder)
|
||||
|
||||
def _dispatch_first_found_default_or_lookup(self, state, remainder):
|
||||
"""
|
||||
When the dispatch has reached the end of the tree but not found an
|
||||
applicable method, so therefore we head back up the branches of the
|
||||
tree until we found a method which matches with a default or lookup method.
|
||||
"""
|
||||
orig_url_path = state.url_path
|
||||
if len(remainder):
|
||||
state.url_path = state.url_path[:-len(remainder)]
|
||||
for i in xrange(len(state.controller_path)):
|
||||
controller = state.controller
|
||||
if self._is_exposed(controller, '_default'):
|
||||
state.add_method(controller._default, remainder)
|
||||
state.dispatcher = self
|
||||
return state
|
||||
if self._is_exposed(controller, '_lookup'):
|
||||
controller, remainder = controller._lookup(*remainder)
|
||||
state.url_path = '/'.join(remainder)
|
||||
return self._dispatch_controller(
|
||||
'_lookup', controller, state, remainder)
|
||||
state.controller_path.pop()
|
||||
if len(state.url_path):
|
||||
remainder = list(remainder)
|
||||
remainder.insert(0, state.url_path[-1])
|
||||
state.url_path.pop()
|
||||
raise HTTPNotFound
|
||||
|
||||
def _dispatch(self, state, remainder):
|
||||
"""
|
||||
This method defines how the object dispatch mechanism works, including
|
||||
checking for security along the way.
|
||||
"""
|
||||
current_controller = state.controller
|
||||
|
||||
if hasattr(current_controller, '_check_security'):
|
||||
current_controller._check_security()
|
||||
#we are plumb out of path, check for index
|
||||
if not remainder:
|
||||
if hasattr(current_controller, 'index'):
|
||||
state.add_method(current_controller.index, remainder)
|
||||
return state
|
||||
#if there is no index, head up the tree
|
||||
#to see if there is a default or lookup method we can use
|
||||
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||
|
||||
current_path = remainder[0]
|
||||
|
||||
#an exposed method matching the path is found
|
||||
if self._is_exposed(current_controller, current_path):
|
||||
#check to see if the argspec jives
|
||||
controller = getattr(current_controller, current_path)
|
||||
if self._method_matches_args(controller, state, remainder[1:]):
|
||||
state.add_method(controller, remainder[1:])
|
||||
return state
|
||||
|
||||
#another controller is found
|
||||
if hasattr(current_controller, current_path):
|
||||
current_controller = getattr(current_controller, current_path)
|
||||
return self._dispatch_controller(
|
||||
current_path, current_controller, state, remainder[1:])
|
||||
|
||||
#dispatch not found
|
||||
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||
|
||||
def _setup_wsgiorg_routing_args(self, url_path, remainder, params):
|
||||
"""
|
||||
This is expected to be overridden by any subclass that wants to set
|
||||
the routing_args (RestController). Do not delete.
|
||||
"""
|
43
crank/restcontroller.py
Normal file
43
crank/restcontroller.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""
|
||||
"""
|
||||
import web.core
|
||||
from restdispatcher import RestDispatcher
|
||||
from dispatcher import DispatchState
|
||||
import mimetypes
|
||||
|
||||
class RestController(RestDispatcher):
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
verb = kw.get('_method', None)
|
||||
|
||||
request = web.core.request
|
||||
url_path = '/'.join(args)
|
||||
state = DispatchState(url_path, kw)
|
||||
state.request = request
|
||||
state.add_controller('/', self)
|
||||
state.dispatcher = self
|
||||
state = state.controller._dispatch(state, args)
|
||||
|
||||
verb = kw.pop('_verb', request.method).lower()
|
||||
|
||||
# attach the request to the controller for use without the
|
||||
# cost of a SOP.
|
||||
# also, save the dispatch state
|
||||
try:
|
||||
state.controller.request
|
||||
state.controller.dispatch_state
|
||||
except AttributeError:
|
||||
state.controller.request = request
|
||||
state.controller.dispatch_state = state
|
||||
|
||||
return state.method(*state.remainder, **kw)
|
||||
|
||||
def __before__(self, *args, **kw):
|
||||
return (args, kw)
|
||||
|
||||
def __after__(self, result, *args, **kw):
|
||||
"""The __after__ method can modify the value returned by the final method call."""
|
||||
return result
|
||||
|
||||
index = __call__
|
||||
__default__ = __call__
|
228
crank/restdispatcher.py
Normal file
228
crank/restdispatcher.py
Normal file
@ -0,0 +1,228 @@
|
||||
"""
|
||||
This module contains the RestDispatcher implementation
|
||||
|
||||
Rest controller provides a RESTful dispatch mechanism, and
|
||||
combines controller decoration for TG-Controller behavior.
|
||||
"""
|
||||
from webob.exc import HTTPMethodNotAllowed
|
||||
from objectdispatcher import ObjectDispatcher
|
||||
|
||||
class RestDispatcher(ObjectDispatcher):
|
||||
"""Defines a restful interface for a set of HTTP verbs.
|
||||
Please see RestController for a rundown of the controller
|
||||
methods used.
|
||||
"""
|
||||
def _setup_wsgiorg_routing_args(self, url_path, remainder, params):
|
||||
pass
|
||||
#request.environ['wsgiorg.routing_args'] = (tuple(remainder), params)
|
||||
|
||||
def _handle_put_or_post(self, method, state, remainder):
|
||||
current_controller = state.controller
|
||||
if remainder:
|
||||
current_path = remainder[0]
|
||||
if self._is_exposed(current_controller, current_path):
|
||||
state.add_method(getattr(current_controller, current_path), remainder[1:])
|
||||
return state
|
||||
|
||||
if self._is_controller(current_controller, current_path):
|
||||
current_controller = getattr(current_controller, current_path)
|
||||
return self._dispatch_controller(current_path, current_controller, state, remainder[1:])
|
||||
|
||||
method_name = method
|
||||
method = self._find_first_exposed(current_controller, [method,])
|
||||
if method and self._method_matches_args(method, state, remainder):
|
||||
state.add_method(method, remainder)
|
||||
return state
|
||||
|
||||
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||
|
||||
def _handle_delete(self, method, state, remainder):
|
||||
current_controller = state.controller
|
||||
method_name = method
|
||||
method = self._find_first_exposed(current_controller, ('post_delete', 'delete'))
|
||||
|
||||
if method and self._method_matches_args(method, state, remainder):
|
||||
state.add_method(method, remainder)
|
||||
return state
|
||||
|
||||
#you may not send a delete request to a non-delete function
|
||||
if remainder and self._is_exposed(current_controller, remainder[0]):
|
||||
raise HTTPMethodNotAllowed
|
||||
|
||||
# there might be a sub-controller with a delete method, let's go see
|
||||
if remainder:
|
||||
sub_controller = getattr(current_controller, remainder[0], None)
|
||||
if sub_controller:
|
||||
remainder = remainder[1:]
|
||||
state.current_controller = sub_controller
|
||||
state.url_path = '/'.join(remainder)
|
||||
r = self._dispatch_controller(state.url_path, sub_controller, state, remainder)
|
||||
if r:
|
||||
return r
|
||||
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||
|
||||
def _check_for_sub_controllers(self, state, remainder):
|
||||
current_controller = state.controller
|
||||
method = None
|
||||
for find in ('get_one', 'get'):
|
||||
if hasattr(current_controller, find):
|
||||
method = find
|
||||
break
|
||||
if method is None:
|
||||
return
|
||||
args = self._get_argspec(getattr(current_controller, method))
|
||||
fixed_args = args[0][1:]
|
||||
fixed_arg_length = len(fixed_args)
|
||||
var_args = args[1]
|
||||
if var_args:
|
||||
for i, item in enumerate(remainder):
|
||||
if hasattr(current_controller, item) and self._is_controller(current_controller, item):
|
||||
current_controller = getattr(current_controller, item)
|
||||
state.add_routing_args(item, remainder[:i], fixed_args, var_args)
|
||||
return self._dispatch_controller(item, current_controller, state, remainder[i+1:])
|
||||
elif fixed_arg_length< len(remainder) and hasattr(current_controller, remainder[fixed_arg_length]):
|
||||
item = remainder[fixed_arg_length]
|
||||
if hasattr(current_controller, item):
|
||||
if self._is_controller(current_controller, item):
|
||||
state.add_routing_args(item, remainder, fixed_args, var_args)
|
||||
return self._dispatch_controller(item, getattr(current_controller, item), state, remainder[fixed_arg_length+1:])
|
||||
|
||||
def _handle_delete_edit_or_new(self, state, remainder):
|
||||
method_name = remainder[-1]
|
||||
if method_name not in ('new', 'edit', 'delete'):
|
||||
return
|
||||
if method_name == 'delete':
|
||||
method_name = 'get_delete'
|
||||
|
||||
current_controller = state.controller
|
||||
|
||||
if self._is_exposed(current_controller, method_name):
|
||||
method = getattr(current_controller, method_name)
|
||||
new_remainder = remainder[:-1]
|
||||
if method and self._method_matches_args(method, state, new_remainder):
|
||||
state.add_method(method, new_remainder)
|
||||
return state
|
||||
|
||||
def _handle_custom_get(self, state, remainder):
|
||||
method_name = remainder[-1]
|
||||
if method_name not in getattr(self, '_custom_actions', []):
|
||||
return
|
||||
|
||||
current_controller = state.controller
|
||||
|
||||
if (self._is_exposed(current_controller, method_name) or
|
||||
self._is_exposed(current_controller, 'get_%s' % method_name)):
|
||||
method = self._find_first_exposed(current_controller, ('get_%s' % method_name, method_name))
|
||||
new_remainder = remainder[:-1]
|
||||
if method and self._method_matches_args(method, state, new_remainder):
|
||||
state.add_method(method, new_remainder)
|
||||
return state
|
||||
|
||||
def _handle_custom_method(self, method, state, remainder):
|
||||
current_controller = state.controller
|
||||
method_name = method
|
||||
method = self._find_first_exposed(current_controller, ('post_%s' % method_name, method_name))
|
||||
|
||||
if method and self._method_matches_args(method, state, remainder):
|
||||
state.add_method(method, remainder)
|
||||
return state
|
||||
|
||||
#you may not send a delete request to a non-delete function
|
||||
if remainder and self._is_exposed(current_controller, remainder[0]):
|
||||
raise HTTPMethodNotAllowed
|
||||
|
||||
# there might be a sub-controller with a delete method, let's go see
|
||||
if remainder:
|
||||
sub_controller = getattr(current_controller, remainder[0], None)
|
||||
if sub_controller:
|
||||
remainder = remainder[1:]
|
||||
state.current_controller = sub_controller
|
||||
state.url_path = '/'.join(remainder)
|
||||
r = self._dispatch_controller(state.url_path, sub_controller, state, remainder)
|
||||
if r:
|
||||
return r
|
||||
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||
|
||||
def _handle_get(self, method, state, remainder):
|
||||
current_controller = state.controller
|
||||
if not remainder:
|
||||
method = self._find_first_exposed(current_controller, ('get_all', 'get'))
|
||||
if method:
|
||||
state.add_method(method, remainder)
|
||||
return state
|
||||
if self._is_exposed(current_controller, 'get_one'):
|
||||
method = current_controller.get_one
|
||||
if method and self._method_matches_args(method, state, remainder):
|
||||
state.add_method(method, remainder)
|
||||
return state
|
||||
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||
|
||||
#test for "delete", "edit" or "new"
|
||||
r = self._handle_delete_edit_or_new(state, remainder)
|
||||
if r:
|
||||
return r
|
||||
|
||||
#test for custom REST-like attribute
|
||||
r = self._handle_custom_get(state, remainder)
|
||||
if r:
|
||||
return r
|
||||
|
||||
current_path = remainder[0]
|
||||
if self._is_exposed(current_controller, current_path):
|
||||
state.add_method(getattr(current_controller, current_path), remainder[1:])
|
||||
return state
|
||||
|
||||
if self._is_controller(current_controller, current_path):
|
||||
current_controller = getattr(current_controller, current_path)
|
||||
return self._dispatch_controller(current_path, current_controller, state, remainder[1:])
|
||||
|
||||
if self._is_exposed(current_controller, 'get_one') or self._is_exposed(current_controller, 'get'):
|
||||
|
||||
if self._is_exposed(current_controller, 'get_one'):
|
||||
method = current_controller.get_one
|
||||
else:
|
||||
method = current_controller.get
|
||||
|
||||
if method and self._method_matches_args(method, state, remainder):
|
||||
state.add_method(method, remainder)
|
||||
return state
|
||||
|
||||
return self._dispatch_first_found_default_or_lookup(state, remainder)
|
||||
|
||||
_handler_lookup = {
|
||||
'put':_handle_put_or_post,
|
||||
'post':_handle_put_or_post,
|
||||
'delete':_handle_delete,
|
||||
'get':_handle_get,
|
||||
}
|
||||
|
||||
def _dispatch(self, state, remainder):
|
||||
"""returns: populated DispachState object
|
||||
"""
|
||||
|
||||
log.debug('Entering dispatch for remainder: %s in controller %s'%(remainder, self))
|
||||
if not hasattr(state, 'http_method'):
|
||||
method = state.request.method.lower()
|
||||
params = state.params
|
||||
|
||||
#conventional hack for handling methods which are not supported by most browsers
|
||||
request_method = params.get('_method', None)
|
||||
if request_method:
|
||||
request_method = request_method.lower()
|
||||
#make certain that DELETE and PUT requests are not sent with GET
|
||||
if method == 'get' and request_method == 'put':
|
||||
raise HTTPMethodNotAllowed
|
||||
if method == 'get' and request_method == 'delete':
|
||||
raise HTTPMethodNotAllowed
|
||||
method = request_method
|
||||
state.http_method = method
|
||||
|
||||
r = self._check_for_sub_controllers(state, remainder)
|
||||
if r:
|
||||
return r
|
||||
|
||||
if state.http_method in self._handler_lookup.keys():
|
||||
r = self._handler_lookup[state.http_method](self, state.http_method, state, remainder)
|
||||
else:
|
||||
r = self._handle_custom_method(state.http_method, state, remainder)
|
||||
return r
|
67
crank/util.py
Normal file
67
crank/util.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""
|
||||
This is the main dispatcher module.
|
||||
|
||||
Dispatch works as follows:
|
||||
Start at the RootController, the root controller must
|
||||
have a _dispatch function, which defines how we move
|
||||
from object to object in the system.
|
||||
Continue following the dispatch mechanism for a given
|
||||
controller until you reach another controller with a
|
||||
_dispatch method defined. Use the new _dispatch
|
||||
method until anther controller with _dispatch defined
|
||||
or until the url has been traversed to entirety.
|
||||
|
||||
This module also contains the standard ObjectDispatch
|
||||
class which provides the ordinary TurboGears mechanism.
|
||||
|
||||
"""
|
||||
|
||||
class odict(dict):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
self._ordering = []
|
||||
dict.__init__(self, *args, **kw)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._ordering.append(key)
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
def keys(self):
|
||||
return self._ordering
|
||||
|
||||
def clear(self):
|
||||
self._ordering = []
|
||||
dict.clear(self)
|
||||
|
||||
def getitem(self, n):
|
||||
return self[self._ordering[n]]
|
||||
|
||||
def __slice__(self, a, b, n):
|
||||
return self.values()[a:b:n]
|
||||
|
||||
def iteritems(self):
|
||||
for item in self._ordering:
|
||||
yield item, self[item]
|
||||
|
||||
def items(self):
|
||||
return [i for i in self.iteritems()]
|
||||
|
||||
def itervalues(self):
|
||||
for item in self._ordering:
|
||||
yield self[item]
|
||||
|
||||
def values(self):
|
||||
return [i for i in self.values()]
|
||||
|
||||
def __delete__(self, key):
|
||||
self._ordering.remove(key)
|
||||
dict.__delete__(self, key)
|
||||
|
||||
def pop(self):
|
||||
item = self._ordering[-1]
|
||||
del self[item]
|
||||
self._ordering.remove(item)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.items())
|
||||
|
26
setup.py
Normal file
26
setup.py
Normal file
@ -0,0 +1,26 @@
|
||||
from setuptools import setup, find_packages
|
||||
import sys, os
|
||||
|
||||
version = '0.1a1'
|
||||
|
||||
setup(name='crank',
|
||||
version=version,
|
||||
description="Generalization of dispatch mechanism for use across frameworks.",
|
||||
long_description="""\
|
||||
""",
|
||||
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
keywords='',
|
||||
author='Christopher Perkins',
|
||||
author_email='chris@percious.com',
|
||||
url='',
|
||||
license='MIT',
|
||||
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
|
||||
include_package_data=True,
|
||||
zip_safe=True,
|
||||
install_requires=[
|
||||
# -*- Extra requirements: -*-
|
||||
],
|
||||
entry_points="""
|
||||
# -*- Entry points: -*-
|
||||
""",
|
||||
)
|
Loading…
Reference in New Issue
Block a user