0001"""
0002Implements Format objects, which represent different formats and
0003serializations.  Concrete implementations instantiate ``Format``.
0004"""
0005
0006from paste.response import header_value
0007from httpencode.wrappers import LazySerialize, ReplacementInput,        FileLengthWrapper, FileAppIterWrapper
0009from httpencode import api
0010
0011_doc_template = """\
0012Format object that produces %(type)s Python objects
0013from mimetypes: %(mimetypes)s"""
0014
0015class Format(object):
0016
0017    """
0018    Instances of this class represent some particular format.  This is
0019    the entry point for both response and request handling for the
0020    format.
0021    """
0022
0023    def __init__(self, name, content_types, type, secure,
0024                 load, dump_iter,
0025                 want_unicode=False, choose_mimetype=None,
0026                 doc=None):
0027        """
0028        name:
0029            A nice readable name for the format.
0030
0031        content_types:
0032            A list of mime types; the first one will be preferred.
0033
0034        type:
0035            A string giving some indication what kind of Python object
0036            this produces.  'python' is just a generic python object;
0037            other kinds of objects should use the module or class
0038            name.
0039
0040        load, dump_iter:
0041            load and dump_iter take two arguments -- either a file
0042            object or the Python object to be dumped, and then a
0043            content type.
0044
0045        want_unicode:
0046            if true, then load and dump produce/consume unicode
0047            objects
0048            
0049        secure:
0050            if true, then this format can decode arbitrary strings without
0051            having to trust the source
0052
0053        choose_mimetype:
0054            if given, this takes a Python object, preferred content
0055            type, and header dict and returns the content type
0056        
0057        doc:
0058            Any extra documentation you want associated with the
0059            object
0060        """
0061        self.name = name
0062        self.content_types = content_types
0063        self.type = type
0064        self.load = load
0065        self.dump_iter = dump_iter
0066        if choose_mimetype is not None:
0067            self.choose_mimetype = choose_mimetype
0068        self.want_unicode = want_unicode
0069        self.secure = secure
0070        if doc is None:
0071            doc = _doc_template % dict(
0072                type=type, mimetypes=', '.join(content_types))
0073        self.__doc__ = doc
0074
0075    def __repr__(self):
0076        return '<Format %s convert %s to type=%s>' % (
0077            self.name, ', '.join(self.content_types), self.type)
0078
0079    def choose_mimetype(self, request_headers, data):
0080        # @@ Should try to look at Accept
0081        return self.content_types[0]
0082
0083    def parse_request(self, environ, trusted=False):
0084        """
0085        Takes the WSGI environment and parses the request for this
0086        format, possibly using a shortcut to avoid decoding.
0087        """
0088        if not trusted and not self.secure:
0089            raise api.InsecureFormatError(
0090                "%r cannot parse untrusted data" % self)
0091        input = environ['wsgi.input']
0092        if 'CONTENT_LENGTH' in environ:
0093            length = int(environ['CONTENT_LENGTH'])
0094        else:
0095            # If we don't have CONTENT_LENGTH then we'll read
0096            # until the end, or more likely hope that we find
0097            # what we need unserialized.
0098            # @@ Or should we default to 0?
0099            length = None
0100        if hasattr(input, 'decoded') and input.decoded[0] == self.type:
0101            return input.decoded[1]
0102        else:
0103            content_type = api.get_mimetype_from_environ(environ)
0104            if hasattr(input, 'httpencode_dump_iter'):
0105                input = FileAppIterWrapper(
0106                    input.httpencode_dump_iter(content_type))
0107            elif length:
0108                input = FileLengthWrapper(input, length)
0109            parsed = self.load(input, content_type)
0110            decoded = (self.type, parsed)
0111            environ['wsgi.input'] = ReplacementInput(
0112                self, decoded)
0113            return parsed
0114
0115    def parse_wsgi_response(self, status, headers, app_iter, trusted=False):
0116        """
0117        Parses the app_iter.
0118
0119        Note: does not call ``app_iter.close()``
0120        """
0121        if not trusted and not self.secure:
0122            raise api.InsecureFormatError(
0123                "%r cannot parse untrusted data" % self)
0124        if hasattr(app_iter, 'decoded'):
0125            type, data = app_iter.decoded
0126            if type == self.type:
0127                return data
0128        content_type = api.get_mimetype_from_headers(headers)
0129        input = FileAppIterWrapper(app_iter)
0130        data = self.load(input, content_type)
0131        return data
0132
0133    def parse_response(self, response):
0134        """
0135        Parses the HTTPResponse-like object
0136        """
0137        content_type = api.get_mimetype_from_response(response)
0138        # Response objects happen to be file-like:
0139        data = self.load(response, content_type)
0140        return data
0141
0142    def make_wsgi_input_length(self, body, headers, internal):
0143        content_type = header_value(headers, 'content-type')
0144        if internal:
0145            return LazySerialize(self, content_type, body), '1'
0146        else:
0147            data = ''.join(self.dump_iter(body, content_type))
0148            return StringIO(data), str(len(data))
0149
0150    def responder(self, data, content_type=None, headers=None):
0151        """
0152        Returns a WSGI application that serves the given data, with an
0153        optional explicit content_type and optional additional
0154        headers.
0155        """
0156        return RPCResponder(self, data, content_type, headers)
0157
0158class RPCResponder(object):
0159
0160    def __init__(self, format, data, content_type=None, headers=None):
0161        self.format = format
0162        self.data = data
0163        if headers is None:
0164            headers = []
0165        elif hasattr(headers, 'items'):
0166            headers = headers.items()
0167        else:
0168            headers = list(headers)
0169        header_content_type = header_value(headers, 'content-type')
0170        if (header_content_type is not None
0171            and content_type is not None
0172            and header_content_type != content_type):
0173            raise TypeError(
0174                "You've given an explicit header of Content-Type: "
0175                "%s and passed content_type=%r, which is ambiguous"
0176                % (header_content_type, content_type))
0177        if header_content_type is None:
0178            if content_type is None:
0179                content_type = format.content_types[0]
0180            headers.append(('Content-Type', content_type))
0181        elif content_type is None:
0182            content_type = header_content_type
0183        self.content_type = content_type
0184        self.headers = headers
0185
0186    def __call__(self, environ, start_response):
0187        start_response('200 OK', self.headers)
0188        # @@: I should check if this is an internal request
0189        return LazySerialize(
0190            self.format, self.content_type, self.data)