1 from __future__ import generators
3 import decimal, re, inspect
7 # yaml isn't standard with python. It shouldn't be required if it
13 # Fallback since `any` isn't in Python <2.5
18 for element in iterable:
23 from django.db.models.query import QuerySet
24 from django.db.models import Model, permalink
25 from django.utils import simplejson
26 from django.utils.xmlutils import SimplerXMLGenerator
27 from django.utils.encoding import smart_unicode
28 from django.core.urlresolvers import reverse, NoReverseMatch
29 from django.core.serializers.json import DateTimeAwareJSONEncoder
30 from django.http import HttpResponse
31 from django.core import serializers
33 from utils import HttpStatusCode, Mimer
36 import cStringIO as StringIO
41 import cPickle as pickle
45 # Allow people to change the reverser (default `permalink`).
48 class Emitter(object):
50 Super emitter. All other emitters should subclass
51 this one. It has the `construct` method which
52 conveniently returns a serialized `dict`. This is
53 usually the only method you want to use in your
54 emitter. See below for examples.
56 `RESERVED_FIELDS` was introduced when better resource
57 method detection came, and we accidentially caught these
58 as the methods on the handler. Issue58 says that's no good.
61 RESERVED_FIELDS = set([ 'read', 'update', 'create',
62 'delete', 'model', 'anonymous',
63 'allowed_methods', 'fields', 'exclude' ])
65 def __init__(self, payload, typemapper, handler, fields=(), anonymous=True):
66 self.typemapper = typemapper
68 self.handler = handler
70 self.anonymous = anonymous
72 if isinstance(self.data, Exception):
75 def method_fields(self, handler, fields):
81 for field in fields - Emitter.RESERVED_FIELDS:
82 t = getattr(handler, str(field), None)
91 Recursively serialize a lot of types, and
92 in cases where it doesn't recognize the type,
93 it will fall back to Django's `smart_unicode`.
97 def _any(thing, fields=()):
99 Dispatch, all types are routed through here.
103 if isinstance(thing, QuerySet):
104 ret = _qs(thing, fields=fields)
105 elif isinstance(thing, (tuple, list)):
107 elif isinstance(thing, dict):
109 elif isinstance(thing, decimal.Decimal):
111 elif isinstance(thing, Model):
112 ret = _model(thing, fields=fields)
113 elif isinstance(thing, HttpResponse):
114 raise HttpStatusCode(thing)
115 elif inspect.isfunction(thing):
116 if not inspect.getargspec(thing)[0]:
118 elif hasattr(thing, '__emittable__'):
119 f = thing.__emittable__
120 if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
122 elif repr(thing).startswith("<django.db.models.fields.related.RelatedManager"):
123 ret = _any(thing.all())
125 ret = smart_unicode(thing, strings_only=True)
129 def _fk(data, field):
133 return _any(getattr(data, field.name))
135 def _related(data, fields=()):
139 return [ _model(m, fields) for m in data.iterator() ]
141 def _m2m(data, field, fields=()):
143 Many to many (re-route to `_model`.)
145 return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
147 def _model(data, fields=()):
149 Models. Will respect the `fields` and/or
150 `exclude` on the handler (see `typemapper`.)
153 handler = self.in_typemapper(type(data), self.anonymous)
154 get_absolute_uri = False
156 if handler or fields:
157 v = lambda f: getattr(data, f.attname)
161 Fields was not specified, try to find teh correct
162 version in the typemapper we were sent.
164 mapped = self.in_typemapper(type(data), self.anonymous)
165 get_fields = set(mapped.fields)
166 exclude_fields = set(mapped.exclude).difference(get_fields)
168 if 'absolute_uri' in get_fields:
169 get_absolute_uri = True
172 get_fields = set([ f.attname.replace("_id", "", 1)
173 for f in data._meta.fields ])
175 # sets can be negated.
176 for exclude in exclude_fields:
177 if isinstance(exclude, basestring):
178 get_fields.discard(exclude)
180 elif isinstance(exclude, re._pattern_type):
181 for field in get_fields.copy():
182 if exclude.match(field):
183 get_fields.discard(field)
186 get_fields = set(fields)
188 met_fields = self.method_fields(handler, get_fields)
190 for f in data._meta.local_fields:
191 if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
193 if f.attname in get_fields:
194 ret[f.attname] = _any(v(f))
195 get_fields.remove(f.attname)
197 if f.attname[:-3] in get_fields:
198 ret[f.name] = _fk(data, f)
199 get_fields.remove(f.name)
201 for mf in data._meta.many_to_many:
202 if mf.serialize and mf.attname not in met_fields:
203 if mf.attname in get_fields:
204 ret[mf.name] = _m2m(data, mf)
205 get_fields.remove(mf.name)
207 # try to get the remainder of fields
208 for maybe_field in get_fields:
209 if isinstance(maybe_field, (list, tuple)):
210 model, fields = maybe_field
211 inst = getattr(data, model, None)
214 if hasattr(inst, 'all'):
215 ret[model] = _related(inst, fields)
217 if len(inspect.getargspec(inst)[0]) == 1:
218 ret[model] = _any(inst(), fields)
220 ret[model] = _model(inst, fields)
222 elif maybe_field in met_fields:
223 # Overriding normal field which has a "resource method"
224 # so you can alter the contents of certain fields without
225 # using different names.
226 ret[maybe_field] = _any(met_fields[maybe_field](data))
229 maybe = getattr(data, maybe_field, None)
232 if len(inspect.getargspec(maybe)[0]) == 1:
233 ret[maybe_field] = _any(maybe())
235 ret[maybe_field] = _any(maybe)
237 handler_f = getattr(handler or self.handler, maybe_field, None)
240 ret[maybe_field] = _any(handler_f(data))
243 for f in data._meta.fields:
244 ret[f.attname] = _any(getattr(data, f.attname))
246 fields = dir(data.__class__) + ret.keys()
247 add_ons = [k for k in dir(data) if k not in fields]
250 ret[k] = _any(getattr(data, k))
253 if self.in_typemapper(type(data), self.anonymous):
254 handler = self.in_typemapper(type(data), self.anonymous)
255 if hasattr(handler, 'resource_uri'):
256 url_id, fields = handler.resource_uri(data)
259 ret['resource_uri'] = reverser( lambda: (url_id, fields) )()
260 except NoReverseMatch, e:
263 if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
264 try: ret['resource_uri'] = data.get_api_url()
268 if hasattr(data, 'get_absolute_url') and get_absolute_uri:
269 try: ret['absolute_uri'] = data.get_absolute_url()
274 def _qs(data, fields=()):
278 return [ _any(v, fields) for v in data ]
284 return [ _any(v) for v in data ]
290 return dict([ (k, _any(v)) for k, v in data.iteritems() ])
292 # Kickstart the seralizin'.
293 return _any(self.data, self.fields)
295 def in_typemapper(self, model, anonymous):
296 for klass, (km, is_anon) in self.typemapper.iteritems():
297 if model is km and is_anon is anonymous:
302 This super emitter does not implement `render`,
303 this is a job for the specific emitter below.
305 raise NotImplementedError("Please implement render.")
307 def stream_render(self, request, stream=True):
309 Tells our patched middleware not to look
310 at the contents, and returns a generator
311 rather than the buffered string. Should be
312 more memory friendly for large datasets.
314 yield self.render(request)
317 def get(cls, format):
319 Gets an emitter, returns the class and a content-type.
321 if cls.EMITTERS.has_key(format):
322 return cls.EMITTERS.get(format)
324 raise ValueError("No emitters found for type %s" % format)
327 def register(cls, name, klass, content_type='text/plain'):
332 - `name`: The name of the emitter ('json', 'xml', 'yaml', ...)
333 - `klass`: The emitter class.
334 - `content_type`: The content type to serve response as.
336 cls.EMITTERS[name] = (klass, content_type)
339 def unregister(cls, name):
341 Remove an emitter from the registry. Useful if you don't
342 want to provide output in one of the built-in emitters.
344 return cls.EMITTERS.pop(name, None)
346 class XMLEmitter(Emitter):
347 def _to_xml(self, xml, data):
348 if isinstance(data, (list, tuple)):
350 xml.startElement("resource", {})
351 self._to_xml(xml, item)
352 xml.endElement("resource")
353 elif isinstance(data, dict):
354 for key, value in data.iteritems():
355 xml.startElement(key, {})
356 self._to_xml(xml, value)
359 xml.characters(smart_unicode(data))
361 def render(self, request):
362 stream = StringIO.StringIO()
364 xml = SimplerXMLGenerator(stream, "utf-8")
366 xml.startElement("response", {})
368 self._to_xml(xml, self.construct())
370 xml.endElement("response")
373 return stream.getvalue()
375 Emitter.register('xml', XMLEmitter, 'text/xml; charset=utf-8')
376 Mimer.register(lambda *a: None, ('text/xml',))
378 class JSONEmitter(Emitter):
380 JSON emitter, understands timestamps.
382 def render(self, request):
383 cb = request.GET.get('callback')
384 seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
388 return '%s(%s)' % (cb, seria)
392 Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')
393 Mimer.register(simplejson.loads, ('application/json',))
395 class YAMLEmitter(Emitter):
397 YAML emitter, uses `safe_dump` to omit the
398 specific types when outputting to non-Python.
400 def render(self, request):
401 return yaml.safe_dump(self.construct())
403 if yaml: # Only register yaml if it was import successfully.
404 Emitter.register('yaml', YAMLEmitter, 'application/x-yaml; charset=utf-8')
405 Mimer.register(lambda s: dict(yaml.load(s)), ('application/x-yaml',))
407 class PickleEmitter(Emitter):
409 Emitter that returns Python pickled.
411 def render(self, request):
412 return pickle.dumps(self.construct())
414 Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
417 WARNING: Accepting arbitrary pickled data is a huge security concern.
418 The unpickler has been disabled by default now, and if you want to use
419 it, please be aware of what implications it will have.
421 Read more: http://nadiana.com/python-pickle-insecure
423 Uncomment the line below to enable it. You're doing so at your own risk.
425 # Mimer.register(pickle.loads, ('application/python-pickle',))
427 class DjangoEmitter(Emitter):
429 Emitter for the Django serialized format.
431 def render(self, request, format='xml'):
432 if isinstance(self.data, HttpResponse):
434 elif isinstance(self.data, (int, str)):
437 response = serializers.serialize(format, self.data, indent=True)
441 Emitter.register('django', DjangoEmitter, 'text/xml; charset=utf-8')