summaryrefslogtreecommitdiffstats
path: root/openstack
diff options
context:
space:
mode:
authorJay Pipes <jaypipes@gmail.com>2011-07-26 09:05:53 -0400
committerJay Pipes <jaypipes@gmail.com>2011-07-26 09:05:53 -0400
commitc85e1f7b4e4e8ea7a4173188123c01eb2b165627 (patch)
treeb1c2142a51100671102c50c990a8b351fdf5389d /openstack
downloadoslo-c85e1f7b4e4e8ea7a4173188123c01eb2b165627.tar.gz
oslo-c85e1f7b4e4e8ea7a4173188123c01eb2b165627.tar.xz
oslo-c85e1f7b4e4e8ea7a4173188123c01eb2b165627.zip
Initial skeleton project
Diffstat (limited to 'openstack')
-rw-r--r--openstack/common/__init__.py19
-rw-r--r--openstack/common/config.py325
-rw-r--r--openstack/common/exception.py145
-rw-r--r--openstack/common/wsgi.py346
4 files changed, 835 insertions, 0 deletions
diff --git a/openstack/common/__init__.py b/openstack/common/__init__.py
new file mode 100644
index 0000000..b22496f
--- /dev/null
+++ b/openstack/common/__init__.py
@@ -0,0 +1,19 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# TODO(jaypipes) Code in this module is intended to be ported to the eventual
+# openstack-common library
diff --git a/openstack/common/config.py b/openstack/common/config.py
new file mode 100644
index 0000000..c9280f0
--- /dev/null
+++ b/openstack/common/config.py
@@ -0,0 +1,325 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Routines for configuring Openstack Projects
+"""
+
+import ConfigParser
+import logging
+import logging.config
+import logging.handlers
+import optparse
+import os
+import re
+import sys
+
+from paste import deploy
+
+DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
+DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+
+def parse_options(parser, cli_args=None):
+ """
+ Returns the parsed CLI options, command to run and its arguments, merged
+ with any same-named options found in a configuration file.
+
+ The function returns a tuple of (options, args), where options is a
+ mapping of option key/str(value) pairs, and args is the set of arguments
+ (not options) supplied on the command-line.
+
+ The reason that the option values are returned as strings only is that
+ ConfigParser and paste.deploy only accept string values...
+
+ :param parser: The option parser
+ :param cli_args: (Optional) Set of arguments to process. If not present,
+ sys.argv[1:] is used.
+ :retval tuple of (options, args)
+ """
+
+ (options, args) = parser.parse_args(cli_args)
+
+ return (vars(options), args)
+
+
+def add_common_options(parser):
+ """
+ Given a supplied optparse.OptionParser, adds an OptionGroup that
+ represents all common configuration options.
+
+ :param parser: optparse.OptionParser
+ """
+ help_text = "The following configuration options are common to "\
+ "all glance programs."
+
+ group = optparse.OptionGroup(parser, "Common Options", help_text)
+ group.add_option('-v', '--verbose', default=False, dest="verbose",
+ action="store_true",
+ help="Print more verbose output")
+ group.add_option('-d', '--debug', default=False, dest="debug",
+ action="store_true",
+ help="Print debugging output")
+ group.add_option('--config-file', default=None, metavar="PATH",
+ help="Path to the config file to use. When not specified "
+ "(the default), we generally look at the first "
+ "argument specified to be a config file, and if "
+ "that is also missing, we search standard "
+ "directories for a config file.")
+ parser.add_option_group(group)
+
+
+def add_log_options(parser):
+ """
+ Given a supplied optparse.OptionParser, adds an OptionGroup that
+ represents all the configuration options around logging.
+
+ :param parser: optparse.OptionParser
+ """
+ help_text = "The following configuration options are specific to logging "\
+ "functionality for this program."
+
+ group = optparse.OptionGroup(parser, "Logging Options", help_text)
+ group.add_option('--log-config', default=None, metavar="PATH",
+ help="If this option is specified, the logging "
+ "configuration file specified is used and overrides "
+ "any other logging options specified. Please see "
+ "the Python logging module documentation for "
+ "details on logging configuration files.")
+ group.add_option('--log-date-format', metavar="FORMAT",
+ default=DEFAULT_LOG_DATE_FORMAT,
+ help="Format string for %(asctime)s in log records. "
+ "Default: %default")
+ group.add_option('--log-file', default=None, metavar="PATH",
+ help="(Optional) Name of log file to output to. "
+ "If not set, logging will go to stdout.")
+ group.add_option("--log-dir", default=None,
+ help="(Optional) The directory to keep log files in "
+ "(will be prepended to --logfile)")
+ parser.add_option_group(group)
+
+
+def setup_logging(options, conf):
+ """
+ Sets up the logging options for a log with supplied name
+
+ :param options: Mapping of typed option key/values
+ :param conf: Mapping of untyped key/values from config file
+ """
+
+ if options.get('log_config', None):
+ # Use a logging configuration file for all settings...
+ if os.path.exists(options['log_config']):
+ logging.config.fileConfig(options['log_config'])
+ return
+ else:
+ raise RuntimeError("Unable to locate specified logging "
+ "config file: %s" % options['log_config'])
+
+ # If either the CLI option or the conf value
+ # is True, we set to True
+ debug = options.get('debug') or \
+ get_option(conf, 'debug', type='bool', default=False)
+ verbose = options.get('verbose') or \
+ get_option(conf, 'verbose', type='bool', default=False)
+ root_logger = logging.root
+ if debug:
+ root_logger.setLevel(logging.DEBUG)
+ elif verbose:
+ root_logger.setLevel(logging.INFO)
+ else:
+ root_logger.setLevel(logging.WARNING)
+
+ # Set log configuration from options...
+ # Note that we use a hard-coded log format in the options
+ # because of Paste.Deploy bug #379
+ # http://trac.pythonpaste.org/pythonpaste/ticket/379
+ log_format = options.get('log_format', DEFAULT_LOG_FORMAT)
+ log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT)
+ formatter = logging.Formatter(log_format, log_date_format)
+
+ logfile = options.get('log_file')
+ if not logfile:
+ logfile = conf.get('log_file')
+
+ if logfile:
+ logdir = options.get('log_dir')
+ if not logdir:
+ logdir = conf.get('log_dir')
+ if logdir:
+ logfile = os.path.join(logdir, logfile)
+ logfile = logging.FileHandler(logfile)
+ logfile.setFormatter(formatter)
+ logfile.setFormatter(formatter)
+ root_logger.addHandler(logfile)
+ else:
+ handler = logging.StreamHandler(sys.stdout)
+ handler.setFormatter(formatter)
+ root_logger.addHandler(handler)
+
+
+def find_config_file(app_name, options, args):
+ """
+ Return the first config file found for an application.
+
+ We search for the paste config file in the following order:
+ * If --config-file option is used, use that
+ * If args[0] is a file, use that
+ * Search for $app.conf in standard directories:
+ * .
+ * ~.glance/
+ * ~
+ * /etc/glance
+ * /etc
+
+ :retval Full path to config file, or None if no config file found
+ """
+
+ fix_path = lambda p: os.path.abspath(os.path.expanduser(p))
+ if options.get('config_file'):
+ if os.path.exists(options['config_file']):
+ return fix_path(options['config_file'])
+ elif args:
+ if os.path.exists(args[0]):
+ return fix_path(args[0])
+
+ # Handle standard directory search for $app_name.conf
+ config_file_dirs = [fix_path(os.getcwd()),
+ fix_path(os.path.join('~', '.glance')),
+ fix_path('~'),
+ '/etc/glance/',
+ '/etc']
+
+ for cfg_dir in config_file_dirs:
+ cfg_file = os.path.join(cfg_dir, '%s.conf' % app_name)
+ if os.path.exists(cfg_file):
+ return cfg_file
+
+
+def load_paste_config(app_name, options, args):
+ """
+ Looks for a config file to use for an app and returns the
+ config file path and a configuration mapping from a paste config file.
+
+ We search for the paste config file in the following order:
+ * If --config-file option is used, use that
+ * If args[0] is a file, use that
+ * Search for $app_name.conf in standard directories:
+ * .
+ * ~.glance/
+ * ~
+ * /etc/glance
+ * /etc
+
+ :param app_name: Name of the application to load config for, or None.
+ None signifies to only load the [DEFAULT] section of
+ the config file.
+ :param options: Set of typed options returned from parse_options()
+ :param args: Command line arguments from argv[1:]
+ :retval Tuple of (conf_file, conf)
+
+ :raises RuntimeError when config file cannot be located or there was a
+ problem loading the configuration file.
+ """
+ conf_file = find_config_file(app_name, options, args)
+ if not conf_file:
+ raise RuntimeError("Unable to locate any configuration file. "
+ "Cannot load application %s" % app_name)
+ try:
+ conf = deploy.appconfig("config:%s" % conf_file, name=app_name)
+ return conf_file, conf
+ except Exception, e:
+ raise RuntimeError("Error trying to load config %s: %s"
+ % (conf_file, e))
+
+
+def load_paste_app(app_name, options, args):
+ """
+ Builds and returns a WSGI app from a paste config file.
+
+ We search for the paste config file in the following order:
+ * If --config-file option is used, use that
+ * If args[0] is a file, use that
+ * Search for $app_name.conf in standard directories:
+ * .
+ * ~.glance/
+ * ~
+ * /etc/glance
+ * /etc
+
+ :param app_name: Name of the application to load
+ :param options: Set of typed options returned from parse_options()
+ :param args: Command line arguments from argv[1:]
+
+ :raises RuntimeError when config file cannot be located or application
+ cannot be loaded from config file
+ """
+ conf_file, conf = load_paste_config(app_name, options, args)
+
+ try:
+ # Setup logging early, supplying both the CLI options and the
+ # configuration mapping from the config file
+ setup_logging(options, conf)
+
+ # We only update the conf dict for the verbose and debug
+ # flags. Everything else must be set up in the conf file...
+ debug = options.get('debug') or \
+ get_option(conf, 'debug', type='bool', default=False)
+ verbose = options.get('verbose') or \
+ get_option(conf, 'verbose', type='bool', default=False)
+ conf['debug'] = debug
+ conf['verbose'] = verbose
+
+ # Log the options used when starting if we're in debug mode...
+ if debug:
+ logger = logging.getLogger(app_name)
+ logger.debug("*" * 80)
+ logger.debug("Configuration options gathered from config file:")
+ logger.debug(conf_file)
+ logger.debug("================================================")
+ items = dict([(k, v) for k, v in conf.items()
+ if k not in ('__file__', 'here')])
+ for key, value in sorted(items.items()):
+ logger.debug("%(key)-30s %(value)s" % locals())
+ logger.debug("*" * 80)
+ app = deploy.loadapp("config:%s" % conf_file, name=app_name)
+ except (LookupError, ImportError), e:
+ raise RuntimeError("Unable to load %(app_name)s from "
+ "configuration file %(conf_file)s."
+ "\nGot: %(e)r" % locals())
+ return conf, app
+
+
+def get_option(options, option, **kwargs):
+ if option in options:
+ value = options[option]
+ type_ = kwargs.get('type', 'str')
+ if type_ == 'bool':
+ if hasattr(value, 'lower'):
+ return value.lower() == 'true'
+ else:
+ return value
+ elif type_ == 'int':
+ return int(value)
+ elif type_ == 'float':
+ return float(value)
+ else:
+ return value
+ elif 'default' in kwargs:
+ return kwargs['default']
+ else:
+ raise KeyError("option '%s' not found" % option)
diff --git a/openstack/common/exception.py b/openstack/common/exception.py
new file mode 100644
index 0000000..ed6b039
--- /dev/null
+++ b/openstack/common/exception.py
@@ -0,0 +1,145 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Exceptions common to OpenStack projects
+"""
+
+import logging
+import sys
+import traceback
+
+
+class ProcessExecutionError(IOError):
+ def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
+ description=None):
+ if description is None:
+ description = "Unexpected error while running command."
+ if exit_code is None:
+ exit_code = '-'
+ message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (
+ description, cmd, exit_code, stdout, stderr)
+ IOError.__init__(self, message)
+
+
+class Error(Exception):
+ def __init__(self, message=None):
+ super(Error, self).__init__(message)
+
+
+class ApiError(Error):
+ def __init__(self, message='Unknown', code='Unknown'):
+ self.message = message
+ self.code = code
+ super(ApiError, self).__init__('%s: %s' % (code, message))
+
+
+class NotFound(Error):
+ pass
+
+
+class UnknownScheme(Error):
+
+ msg = "Unknown scheme '%s' found in URI"
+
+ def __init__(self, scheme):
+ msg = self.__class__.msg % scheme
+ super(UnknownScheme, self).__init__(msg)
+
+
+class BadStoreUri(Error):
+
+ msg = "The Store URI %s was malformed. Reason: %s"
+
+ def __init__(self, uri, reason):
+ msg = self.__class__.msg % (uri, reason)
+ super(BadStoreUri, self).__init__(msg)
+
+
+class Duplicate(Error):
+ pass
+
+
+class NotAuthorized(Error):
+ pass
+
+
+class NotEmpty(Error):
+ pass
+
+
+class Invalid(Error):
+ pass
+
+
+class BadInputError(Exception):
+ """Error resulting from a client sending bad input to a server"""
+ pass
+
+
+class MissingArgumentError(Error):
+ pass
+
+
+class DatabaseMigrationError(Error):
+ pass
+
+
+class ClientConnectionError(Exception):
+ """Error resulting from a client connecting to a server"""
+ pass
+
+
+def wrap_exception(f):
+ def _wrap(*args, **kw):
+ try:
+ return f(*args, **kw)
+ except Exception, e:
+ if not isinstance(e, Error):
+ #exc_type, exc_value, exc_traceback = sys.exc_info()
+ logging.exception('Uncaught exception')
+ #logging.error(traceback.extract_stack(exc_traceback))
+ raise Error(str(e))
+ raise
+ _wrap.func_name = f.func_name
+ return _wrap
+
+
+class GlanceException(Exception):
+ """
+ Base Glance Exception
+
+ To correctly use this class, inherit from it and define
+ a 'message' property. That message will get printf'd
+ with the keyword arguments provided to the constructor.
+ """
+ message = "An unknown exception occurred"
+
+ def __init__(self, **kwargs):
+ try:
+ self._error_string = self.message % kwargs
+
+ except Exception:
+ # at least get the core message out if something happened
+ self._error_string = self.message
+
+ def __str__(self):
+ return self._error_string
+
+
+class InvalidContentType(GlanceException):
+ message = "Invalid content type %(content_type)s"
diff --git a/openstack/common/wsgi.py b/openstack/common/wsgi.py
new file mode 100644
index 0000000..ef132dc
--- /dev/null
+++ b/openstack/common/wsgi.py
@@ -0,0 +1,346 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Utility methods for working with WSGI servers
+"""
+
+import json
+import logging
+import sys
+import datetime
+
+import eventlet
+import eventlet.wsgi
+eventlet.patcher.monkey_patch(all=False, socket=True)
+import routes
+import routes.middleware
+import webob.dec
+import webob.exc
+
+from openstack.common import exception
+
+
+class WritableLogger(object):
+ """A thin wrapper that responds to `write` and logs."""
+
+ def __init__(self, logger, level=logging.DEBUG):
+ self.logger = logger
+ self.level = level
+
+ def write(self, msg):
+ self.logger.log(self.level, msg.strip("\n"))
+
+
+def run_server(application, port):
+ """Run a WSGI server with the given application."""
+ sock = eventlet.listen(('0.0.0.0', port))
+ eventlet.wsgi.server(sock, application)
+
+
+class Server(object):
+ """Server class to manage multiple WSGI sockets and applications."""
+
+ def __init__(self, threads=1000):
+ self.pool = eventlet.GreenPool(threads)
+
+ def start(self, application, port, host='0.0.0.0', backlog=128):
+ """Run a WSGI server with the given application."""
+ socket = eventlet.listen((host, port), backlog=backlog)
+ self.pool.spawn_n(self._run, application, socket)
+
+ def wait(self):
+ """Wait until all servers have completed running."""
+ try:
+ self.pool.waitall()
+ except KeyboardInterrupt:
+ pass
+
+ def _run(self, application, socket):
+ """Start a WSGI server in a new green thread."""
+ logger = logging.getLogger('eventlet.wsgi.server')
+ eventlet.wsgi.server(socket, application, custom_pool=self.pool,
+ log=WritableLogger(logger))
+
+
+class Middleware(object):
+ """
+ Base WSGI middleware wrapper. These classes require an application to be
+ initialized that will be called next. By default the middleware will
+ simply call its wrapped app, or you can override __call__ to customize its
+ behavior.
+ """
+
+ def __init__(self, application):
+ self.application = application
+
+ def process_request(self, req):
+ """
+ Called on each request.
+
+ If this returns None, the next application down the stack will be
+ executed. If it returns a response then that response will be returned
+ and execution will stop here.
+ """
+ return None
+
+ def process_response(self, response):
+ """Do whatever you'd like to the response."""
+ return response
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ response = self.process_request(req)
+ if response:
+ return response
+ response = req.get_response(self.application)
+ return self.process_response(response)
+
+
+class Debug(Middleware):
+ """
+ Helper class that can be inserted into any WSGI application chain
+ to get information about the request and response.
+ """
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ print ("*" * 40) + " REQUEST ENVIRON"
+ for key, value in req.environ.items():
+ print key, "=", value
+ print
+ resp = req.get_response(self.application)
+
+ print ("*" * 40) + " RESPONSE HEADERS"
+ for (key, value) in resp.headers.iteritems():
+ print key, "=", value
+ print
+
+ resp.app_iter = self.print_generator(resp.app_iter)
+
+ return resp
+
+ @staticmethod
+ def print_generator(app_iter):
+ """
+ Iterator that prints the contents of a wrapper string iterator
+ when iterated.
+ """
+ print ("*" * 40) + " BODY"
+ for part in app_iter:
+ sys.stdout.write(part)
+ sys.stdout.flush()
+ yield part
+ print
+
+
+class Router(object):
+
+ """
+ WSGI middleware that maps incoming requests to WSGI apps.
+ """
+
+ def __init__(self, mapper):
+ """
+ Create a router for the given routes.Mapper.
+
+ Each route in `mapper` must specify a 'controller', which is a
+ WSGI app to call. You'll probably want to specify an 'action' as
+ well and have your controller be a wsgi.Controller, who will route
+ the request to the action method.
+
+ Examples:
+ mapper = routes.Mapper()
+ sc = ServerController()
+
+ # Explicit mapping of one route to a controller+action
+ mapper.connect(None, "/svrlist", controller=sc, action="list")
+
+ # Actions are all implicitly defined
+ mapper.resource("server", "servers", controller=sc)
+
+ # Pointing to an arbitrary WSGI app. You can specify the
+ # {path_info:.*} parameter so the target app can be handed just that
+ # section of the URL.
+ mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
+ """
+ self.map = mapper
+ self._router = routes.middleware.RoutesMiddleware(self._dispatch,
+ self.map)
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """
+ Route the incoming request to a controller based on self.map.
+ If no match, return a 404.
+ """
+ return self._router
+
+ @staticmethod
+ @webob.dec.wsgify
+ def _dispatch(req):
+ """
+ Called by self._router after matching the incoming request to a route
+ and putting the information into req.environ. Either returns 404
+ or the routed WSGI app's response.
+ """
+ match = req.environ['wsgiorg.routing_args'][1]
+ if not match:
+ return webob.exc.HTTPNotFound()
+ app = match['controller']
+ return app
+
+
+class Request(webob.Request):
+
+ """Add some Openstack API-specific logic to the base webob.Request."""
+
+ def best_match_content_type(self):
+ """Determine the requested response content-type."""
+ supported = ('application/json',)
+ bm = self.accept.best_match(supported)
+ return bm or 'application/json'
+
+ def get_content_type(self, allowed_content_types):
+ """Determine content type of the request body."""
+ if not "Content-Type" in self.headers:
+ raise exception.InvalidContentType(content_type=None)
+
+ content_type = self.content_type
+
+ if content_type not in allowed_content_types:
+ raise exception.InvalidContentType(content_type=content_type)
+ else:
+ return content_type
+
+
+class JSONRequestDeserializer(object):
+ def has_body(self, request):
+ """
+ Returns whether a Webob.Request object will possess an entity body.
+
+ :param request: Webob.Request object
+ """
+ if 'transfer-encoding' in request.headers:
+ return True
+ elif request.content_length > 0:
+ return True
+
+ return False
+
+ def from_json(self, datastring):
+ return json.loads(datastring)
+
+ def default(self, request):
+ if self.has_body(request):
+ return {'body': self.from_json(request.body)}
+ else:
+ return {}
+
+
+class JSONResponseSerializer(object):
+
+ def to_json(self, data):
+ def sanitizer(obj):
+ if isinstance(obj, datetime.datetime):
+ return obj.isoformat()
+ return obj
+
+ return json.dumps(data, default=sanitizer)
+
+ def default(self, response, result):
+ response.headers.add('Content-Type', 'application/json')
+ response.body = self.to_json(result)
+
+
+class Resource(object):
+ """
+ WSGI app that handles (de)serialization and controller dispatch.
+
+ Reads routing information supplied by RoutesMiddleware and calls
+ the requested action method upon its deserializer, controller,
+ and serializer. Those three objects may implement any of the basic
+ controller action methods (create, update, show, index, delete)
+ along with any that may be specified in the api router. A 'default'
+ method may also be implemented to be used in place of any
+ non-implemented actions. Deserializer methods must accept a request
+ argument and return a dictionary. Controller methods must accept a
+ request argument. Additionally, they must also accept keyword
+ arguments that represent the keys returned by the Deserializer. They
+ may raise a webob.exc exception or return a dict, which will be
+ serialized by requested content type.
+ """
+ def __init__(self, controller, deserializer, serializer):
+ """
+ :param controller: object that implement methods created by routes lib
+ :param deserializer: object that supports webob request deserialization
+ through controller-like actions
+ :param serializer: object that supports webob response serialization
+ through controller-like actions
+ """
+ self.controller = controller
+ self.serializer = serializer
+ self.deserializer = deserializer
+
+ @webob.dec.wsgify(RequestClass=Request)
+ def __call__(self, request):
+ """WSGI method that controls (de)serialization and method dispatch."""
+ action_args = self.get_action_args(request.environ)
+ action = action_args.pop('action', None)
+
+ deserialized_request = self.dispatch(self.deserializer,
+ action, request)
+ action_args.update(deserialized_request)
+
+ action_result = self.dispatch(self.controller, action,
+ request, **action_args)
+ try:
+ response = webob.Response()
+ self.dispatch(self.serializer, action, response, action_result)
+ return response
+
+ # return unserializable result (typically a webob exc)
+ except Exception:
+ return action_result
+
+ def dispatch(self, obj, action, *args, **kwargs):
+ """Find action-specific method on self and call it."""
+ try:
+ method = getattr(obj, action)
+ except AttributeError:
+ method = getattr(obj, 'default')
+
+ return method(*args, **kwargs)
+
+ def get_action_args(self, request_environment):
+ """Parse dictionary created by routes library."""
+ try:
+ args = request_environment['wsgiorg.routing_args'][1].copy()
+ except Exception:
+ return {}
+
+ try:
+ del args['controller']
+ except KeyError:
+ pass
+
+ try:
+ del args['format']
+ except KeyError:
+ pass
+
+ return args