+from __future__ import generators
+
+import decimal, re, inspect
+import copy
+
+try:
+ # yaml isn't standard with python. It shouldn't be required if it
+ # isn't used.
+ import yaml
+except ImportError:
+ yaml = None
+
+# Fallback since `any` isn't in Python <2.5
+try:
+ any
+except NameError:
+ def any(iterable):
+ for element in iterable:
+ if element:
+ return True
+ return False
+
+from django.db.models.query import QuerySet
+from django.db.models import Model, permalink
+from django.utils import simplejson
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.utils.encoding import smart_unicode
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.core.serializers.json import DateTimeAwareJSONEncoder
+from django.http import HttpResponse
+from django.core import serializers
+
+from utils import HttpStatusCode, Mimer
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+# Allow people to change the reverser (default `permalink`).
+reverser = permalink
+
+class Emitter(object):
+ """
+ Super emitter. All other emitters should subclass
+ this one. It has the `construct` method which
+ conveniently returns a serialized `dict`. This is
+ usually the only method you want to use in your
+ emitter. See below for examples.
+
+ `RESERVED_FIELDS` was introduced when better resource
+ method detection came, and we accidentially caught these
+ as the methods on the handler. Issue58 says that's no good.
+ """
+ EMITTERS = { }
+ RESERVED_FIELDS = set([ 'read', 'update', 'create',
+ 'delete', 'model', 'anonymous',
+ 'allowed_methods', 'fields', 'exclude' ])
+
+ def __init__(self, payload, typemapper, handler, fields=(), anonymous=True):
+ self.typemapper = typemapper
+ self.data = payload
+ self.handler = handler
+ self.fields = fields
+ self.anonymous = anonymous
+
+ if isinstance(self.data, Exception):
+ raise
+
+ def method_fields(self, handler, fields):
+ if not handler:
+ return { }
+
+ ret = dict()
+
+ for field in fields - Emitter.RESERVED_FIELDS:
+ t = getattr(handler, str(field), None)
+
+ if t and callable(t):
+ ret[field] = t
+
+ return ret
+
+ def construct(self):
+ """
+ Recursively serialize a lot of types, and
+ in cases where it doesn't recognize the type,
+ it will fall back to Django's `smart_unicode`.
+
+ Returns `dict`.
+ """
+ def _any(thing, fields=()):
+ """
+ Dispatch, all types are routed through here.
+ """
+ ret = None
+
+ if isinstance(thing, QuerySet):
+ ret = _qs(thing, fields=fields)
+ elif isinstance(thing, (tuple, list)):
+ ret = _list(thing)
+ elif isinstance(thing, dict):
+ ret = _dict(thing)
+ elif isinstance(thing, decimal.Decimal):
+ ret = str(thing)
+ elif isinstance(thing, Model):
+ ret = _model(thing, fields=fields)
+ elif isinstance(thing, HttpResponse):
+ raise HttpStatusCode(thing)
+ elif inspect.isfunction(thing):
+ if not inspect.getargspec(thing)[0]:
+ ret = _any(thing())
+ elif hasattr(thing, '__emittable__'):
+ f = thing.__emittable__
+ if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
+ ret = _any(f())
+ elif repr(thing).startswith("<django.db.models.fields.related.RelatedManager"):
+ ret = _any(thing.all())
+ else:
+ ret = smart_unicode(thing, strings_only=True)
+
+ return ret
+
+ def _fk(data, field):
+ """
+ Foreign keys.
+ """
+ return _any(getattr(data, field.name))
+
+ def _related(data, fields=()):
+ """
+ Foreign keys.
+ """
+ return [ _model(m, fields) for m in data.iterator() ]
+
+ def _m2m(data, field, fields=()):
+ """
+ Many to many (re-route to `_model`.)
+ """
+ return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
+
+ def _model(data, fields=()):
+ """
+ Models. Will respect the `fields` and/or
+ `exclude` on the handler (see `typemapper`.)
+ """
+ ret = { }
+ handler = self.in_typemapper(type(data), self.anonymous)
+ get_absolute_uri = False
+
+ if handler or fields:
+ v = lambda f: getattr(data, f.attname)
+
+ if not fields:
+ """
+ Fields was not specified, try to find teh correct
+ version in the typemapper we were sent.
+ """
+ mapped = self.in_typemapper(type(data), self.anonymous)
+ get_fields = set(mapped.fields)
+ exclude_fields = set(mapped.exclude).difference(get_fields)
+
+ if 'absolute_uri' in get_fields:
+ get_absolute_uri = True
+
+ if not get_fields:
+ get_fields = set([ f.attname.replace("_id", "", 1)
+ for f in data._meta.fields ])
+
+ # sets can be negated.
+ for exclude in exclude_fields:
+ if isinstance(exclude, basestring):
+ get_fields.discard(exclude)
+
+ elif isinstance(exclude, re._pattern_type):
+ for field in get_fields.copy():
+ if exclude.match(field):
+ get_fields.discard(field)
+
+ else:
+ get_fields = set(fields)
+
+ met_fields = self.method_fields(handler, get_fields)
+
+ for f in data._meta.local_fields:
+ if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
+ if not f.rel:
+ if f.attname in get_fields:
+ ret[f.attname] = _any(v(f))
+ get_fields.remove(f.attname)
+ else:
+ if f.attname[:-3] in get_fields:
+ ret[f.name] = _fk(data, f)
+ get_fields.remove(f.name)
+
+ for mf in data._meta.many_to_many:
+ if mf.serialize and mf.attname not in met_fields:
+ if mf.attname in get_fields:
+ ret[mf.name] = _m2m(data, mf)
+ get_fields.remove(mf.name)
+
+ # try to get the remainder of fields
+ for maybe_field in get_fields:
+ if isinstance(maybe_field, (list, tuple)):
+ model, fields = maybe_field
+ inst = getattr(data, model, None)
+
+ if inst:
+ if hasattr(inst, 'all'):
+ ret[model] = _related(inst, fields)
+ elif callable(inst):
+ if len(inspect.getargspec(inst)[0]) == 1:
+ ret[model] = _any(inst(), fields)
+ else:
+ ret[model] = _model(inst, fields)
+
+ elif maybe_field in met_fields:
+ # Overriding normal field which has a "resource method"
+ # so you can alter the contents of certain fields without
+ # using different names.
+ ret[maybe_field] = _any(met_fields[maybe_field](data))
+
+ else:
+ maybe = getattr(data, maybe_field, None)
+ if maybe:
+ if callable(maybe):
+ if len(inspect.getargspec(maybe)[0]) == 1:
+ ret[maybe_field] = _any(maybe())
+ else:
+ ret[maybe_field] = _any(maybe)
+ else:
+ handler_f = getattr(handler or self.handler, maybe_field, None)
+
+ if handler_f:
+ ret[maybe_field] = _any(handler_f(data))
+
+ else:
+ for f in data._meta.fields:
+ ret[f.attname] = _any(getattr(data, f.attname))
+
+ fields = dir(data.__class__) + ret.keys()
+ add_ons = [k for k in dir(data) if k not in fields]
+
+ for k in add_ons:
+ ret[k] = _any(getattr(data, k))
+
+ # resouce uri
+ if self.in_typemapper(type(data), self.anonymous):
+ handler = self.in_typemapper(type(data), self.anonymous)
+ if hasattr(handler, 'resource_uri'):
+ url_id, fields = handler.resource_uri(data)
+
+ try:
+ ret['resource_uri'] = reverser( lambda: (url_id, fields) )()
+ except NoReverseMatch, e:
+ pass
+
+ if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
+ try: ret['resource_uri'] = data.get_api_url()
+ except: pass
+
+ # absolute uri
+ if hasattr(data, 'get_absolute_url') and get_absolute_uri:
+ try: ret['absolute_uri'] = data.get_absolute_url()
+ except: pass
+
+ return ret
+
+ def _qs(data, fields=()):
+ """
+ Querysets.
+ """
+ return [ _any(v, fields) for v in data ]
+
+ def _list(data):
+ """
+ Lists.
+ """
+ return [ _any(v) for v in data ]
+
+ def _dict(data):
+ """
+ Dictionaries.
+ """
+ return dict([ (k, _any(v)) for k, v in data.iteritems() ])
+
+ # Kickstart the seralizin'.
+ return _any(self.data, self.fields)
+
+ def in_typemapper(self, model, anonymous):
+ for klass, (km, is_anon) in self.typemapper.iteritems():
+ if model is km and is_anon is anonymous:
+ return klass
+
+ def render(self):
+ """
+ This super emitter does not implement `render`,
+ this is a job for the specific emitter below.
+ """
+ raise NotImplementedError("Please implement render.")
+
+ def stream_render(self, request, stream=True):
+ """
+ Tells our patched middleware not to look
+ at the contents, and returns a generator
+ rather than the buffered string. Should be
+ more memory friendly for large datasets.
+ """
+ yield self.render(request)
+
+ @classmethod
+ def get(cls, format):
+ """
+ Gets an emitter, returns the class and a content-type.
+ """
+ if cls.EMITTERS.has_key(format):
+ return cls.EMITTERS.get(format)
+
+ raise ValueError("No emitters found for type %s" % format)
+
+ @classmethod
+ def register(cls, name, klass, content_type='text/plain'):
+ """
+ Register an emitter.
+
+ Parameters::
+ - `name`: The name of the emitter ('json', 'xml', 'yaml', ...)
+ - `klass`: The emitter class.
+ - `content_type`: The content type to serve response as.
+ """
+ cls.EMITTERS[name] = (klass, content_type)
+
+ @classmethod
+ def unregister(cls, name):
+ """
+ Remove an emitter from the registry. Useful if you don't
+ want to provide output in one of the built-in emitters.
+ """
+ return cls.EMITTERS.pop(name, None)
+
+class XMLEmitter(Emitter):
+ def _to_xml(self, xml, data):
+ if isinstance(data, (list, tuple)):
+ for item in data:
+ xml.startElement("resource", {})
+ self._to_xml(xml, item)
+ xml.endElement("resource")
+ elif isinstance(data, dict):
+ for key, value in data.iteritems():
+ xml.startElement(key, {})
+ self._to_xml(xml, value)
+ xml.endElement(key)
+ else:
+ xml.characters(smart_unicode(data))
+
+ def render(self, request):
+ stream = StringIO.StringIO()
+
+ xml = SimplerXMLGenerator(stream, "utf-8")
+ xml.startDocument()
+ xml.startElement("response", {})
+
+ self._to_xml(xml, self.construct())
+
+ xml.endElement("response")
+ xml.endDocument()
+
+ return stream.getvalue()
+
+Emitter.register('xml', XMLEmitter, 'text/xml; charset=utf-8')
+Mimer.register(lambda *a: None, ('text/xml',))
+
+class JSONEmitter(Emitter):
+ """
+ JSON emitter, understands timestamps.
+ """
+ def render(self, request):
+ cb = request.GET.get('callback')
+ seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
+
+ # Callback
+ if cb:
+ return '%s(%s)' % (cb, seria)
+
+ return seria
+
+Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')
+Mimer.register(simplejson.loads, ('application/json',))
+
+class YAMLEmitter(Emitter):
+ """
+ YAML emitter, uses `safe_dump` to omit the
+ specific types when outputting to non-Python.
+ """
+ def render(self, request):
+ return yaml.safe_dump(self.construct())
+
+if yaml: # Only register yaml if it was import successfully.
+ Emitter.register('yaml', YAMLEmitter, 'application/x-yaml; charset=utf-8')
+ Mimer.register(lambda s: dict(yaml.load(s)), ('application/x-yaml',))
+
+class PickleEmitter(Emitter):
+ """
+ Emitter that returns Python pickled.
+ """
+ def render(self, request):
+ return pickle.dumps(self.construct())
+
+Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
+
+"""
+WARNING: Accepting arbitrary pickled data is a huge security concern.
+The unpickler has been disabled by default now, and if you want to use
+it, please be aware of what implications it will have.
+
+Read more: http://nadiana.com/python-pickle-insecure
+
+Uncomment the line below to enable it. You're doing so at your own risk.
+"""
+# Mimer.register(pickle.loads, ('application/python-pickle',))
+
+class DjangoEmitter(Emitter):
+ """
+ Emitter for the Django serialized format.
+ """
+ def render(self, request, format='xml'):
+ if isinstance(self.data, HttpResponse):
+ return self.data
+ elif isinstance(self.data, (int, str)):
+ response = self.data
+ else:
+ response = serializers.serialize(format, self.data, indent=True)
+
+ return response
+
+Emitter.register('django', DjangoEmitter, 'text/xml; charset=utf-8')