diff options
Diffstat (limited to 'src/mailman/rest/helpers.py')
| -rw-r--r-- | src/mailman/rest/helpers.py | 112 |
1 files changed, 67 insertions, 45 deletions
diff --git a/src/mailman/rest/helpers.py b/src/mailman/rest/helpers.py index be5d2b565..7257a78fa 100644 --- a/src/mailman/rest/helpers.py +++ b/src/mailman/rest/helpers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010 by the Free Software Foundation, Inc. +# Copyright (C) 2010-2011 by the Free Software Foundation, Inc. # # This file is part of GNU Mailman. # @@ -21,7 +21,7 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ - 'ContainerMixin', + 'PATCH', 'etag', 'no_content', 'path_to', @@ -29,18 +29,21 @@ __all__ = [ ] +import cgi import json import hashlib +from cStringIO import StringIO +from datetime import datetime, timedelta +from flufl.enum import Enum from lazr.config import as_boolean from restish.http import Response +from restish.resource import MethodDecorator +from webob.multidict import MultiDict from mailman.config import config -COMMASPACE = ', ' - - def path_to(resource): """Return the url path to a resource. @@ -61,6 +64,26 @@ def path_to(resource): +class ExtendedEncoder(json.JSONEncoder): + """An extended JSON encoder which knows about other data types.""" + + def default(self, obj): + if isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, timedelta): + # as_timedelta() does not recognize microseconds, so convert these + # to floating seconds, but only if there are any seconds. + if obj.seconds > 0 or obj.microseconds > 0: + seconds = obj.seconds + obj.microseconds / 1000000.0 + return '{0}d{1}s'.format(obj.days, seconds) + return '{0}d'.format(obj.days) + elif hasattr(obj, 'enumclass') and issubclass(obj.enumclass, Enum): + # It's up to the decoding validator to associate this name with + # the right Enum class. + return obj.enumname + return json.JSONEncoder.default(self, obj) + + def etag(resource): """Calculate the etag and return a JSON representation. @@ -78,7 +101,7 @@ def etag(resource): assert 'http_etag' not in resource, 'Resource already etagged' etag = hashlib.sha1(repr(resource)).hexdigest() resource['http_etag'] = '"{0}"'.format(etag) - return json.dumps(resource) + return json.dumps(resource, cls=ExtendedEncoder) @@ -131,45 +154,6 @@ class CollectionMixin: -class Validator: - """A validator of parameter input.""" - - def __init__(self, **kws): - if '_optional' in kws: - self._optional = set(kws.pop('_optional')) - else: - self._optional = set() - self._converters = kws.copy() - - def __call__(self, request): - values = {} - extras = set() - cannot_convert = set() - for key, value in request.POST.items(): - try: - values[key] = self._converters[key](value) - except KeyError: - extras.add(key) - except (TypeError, ValueError): - cannot_convert.add(key) - # Make sure there are no unexpected values. - if len(extras) != 0: - extras = COMMASPACE.join(sorted(extras)) - raise ValueError('Unexpected parameters: {0}'.format(extras)) - # Make sure everything could be converted. - if len(cannot_convert) != 0: - bad = COMMASPACE.join(sorted(cannot_convert)) - raise ValueError('Cannot convert parameters: {0}'.format(bad)) - # Make sure nothing's missing. - value_keys = set(values) - required_keys = set(self._converters) - self._optional - if value_keys & required_keys != required_keys: - missing = COMMASPACE.join(sorted(required_keys - value_keys)) - raise ValueError('Missing parameters: {0}'.format(missing)) - return values - - - # XXX 2010-02-24 barry Seems like contrary to the documentation, matchers # cannot be plain functions, because matchers must have a .score attribute. # OTOH, I think they support regexps, so that might be a better way to go. @@ -183,3 +167,41 @@ def restish_matcher(function): def no_content(): """204 No Content.""" return Response('204 No Content', [], None) + + +# These two classes implement an ugly, dirty hack to work around the fact that +# neither WebOb nor really the stdlib cgi module support non-standard HTTP +# verbs such as PATCH. Note that restish handles it just fine in the sense +# that the right method gets called, but without the following kludge, the +# body of the request will never get decoded, so the method won't see any +# data. +# +# Stuffing the MultiDict on request.PATCH is pretty ugly, but it mirrors +# WebOb's use of request.POST and request.PUT for those standard verbs. +# Besides, WebOb refuses to allow us to set request.POST. This does make +# validators.py a bit more complicated. :( + +class PATCHWrapper: + """Hack to decode the request body for PATCH.""" + def __init__(self, func): + self.func = func + + def __call__(self, resource, request): + # We can't use request.body_file because that's a socket that's + # already had its data read off of. IOW, if we use that directly, + # we'll block here. + field_storage = cgi.FieldStorage( + fp=StringIO(request.body), + # Yes, lie about the method so cgi will do the right thing. + environ=dict(REQUEST_METHOD='POST'), + keep_blank_values=True) + request.PATCH = MultiDict.from_fieldstorage(field_storage) + return self.func(resource, request) + + +class PATCH(MethodDecorator): + method = 'PATCH' + + def __call__(self, func): + really_wrapped_func = PATCHWrapper(func) + return super(PATCH, self).__call__(really_wrapped_func) |
