summaryrefslogtreecommitdiff
path: root/src/mailman/rest/helpers.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/rest/helpers.py')
-rw-r--r--src/mailman/rest/helpers.py112
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)