New design for book-detail template (downloading books etc).
[wolnelektury.git] / apps / piston / emitters.py
1 from __future__ import generators
2
3 import decimal, re, inspect
4 import copy
5
6 try:
7     # yaml isn't standard with python.  It shouldn't be required if it
8     # isn't used.
9     import yaml
10 except ImportError:
11     yaml = None
12
13 # Fallback since `any` isn't in Python <2.5
14 try:
15     any
16 except NameError:
17     def any(iterable):
18         for element in iterable:
19             if element:
20                 return True
21         return False
22
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
32
33 from utils import HttpStatusCode, Mimer
34
35 try:
36     import cStringIO as StringIO
37 except ImportError:
38     import StringIO
39
40 try:
41     import cPickle as pickle
42 except ImportError:
43     import pickle
44
45 # Allow people to change the reverser (default `permalink`).
46 reverser = permalink
47
48 class Emitter(object):
49     """
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.
55
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.
59     """
60     EMITTERS = { }
61     RESERVED_FIELDS = set([ 'read', 'update', 'create',
62                             'delete', 'model', 'anonymous',
63                             'allowed_methods', 'fields', 'exclude' ])
64
65     def __init__(self, payload, typemapper, handler, fields=(), anonymous=True):
66         self.typemapper = typemapper
67         self.data = payload
68         self.handler = handler
69         self.fields = fields
70         self.anonymous = anonymous
71
72         if isinstance(self.data, Exception):
73             raise
74
75     def method_fields(self, handler, fields):
76         if not handler:
77             return { }
78
79         ret = dict()
80
81         for field in fields - Emitter.RESERVED_FIELDS:
82             t = getattr(handler, str(field), None)
83
84             if t and callable(t):
85                 ret[field] = t
86
87         return ret
88
89     def construct(self):
90         """
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`.
94
95         Returns `dict`.
96         """
97         def _any(thing, fields=()):
98             """
99             Dispatch, all types are routed through here.
100             """
101             ret = None
102
103             if isinstance(thing, QuerySet):
104                 ret = _qs(thing, fields=fields)
105             elif isinstance(thing, (tuple, list)):
106                 ret = _list(thing)
107             elif isinstance(thing, dict):
108                 ret = _dict(thing)
109             elif isinstance(thing, decimal.Decimal):
110                 ret = str(thing)
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]:
117                     ret = _any(thing())
118             elif hasattr(thing, '__emittable__'):
119                 f = thing.__emittable__
120                 if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
121                     ret = _any(f())
122             elif repr(thing).startswith("<django.db.models.fields.related.RelatedManager"):
123                 ret = _any(thing.all())
124             else:
125                 ret = smart_unicode(thing, strings_only=True)
126
127             return ret
128
129         def _fk(data, field):
130             """
131             Foreign keys.
132             """
133             return _any(getattr(data, field.name))
134
135         def _related(data, fields=()):
136             """
137             Foreign keys.
138             """
139             return [ _model(m, fields) for m in data.iterator() ]
140
141         def _m2m(data, field, fields=()):
142             """
143             Many to many (re-route to `_model`.)
144             """
145             return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
146
147         def _model(data, fields=()):
148             """
149             Models. Will respect the `fields` and/or
150             `exclude` on the handler (see `typemapper`.)
151             """
152             ret = { }
153             handler = self.in_typemapper(type(data), self.anonymous)
154             get_absolute_uri = False
155
156             if handler or fields:
157                 v = lambda f: getattr(data, f.attname)
158
159                 if not fields:
160                     """
161                     Fields was not specified, try to find teh correct
162                     version in the typemapper we were sent.
163                     """
164                     mapped = self.in_typemapper(type(data), self.anonymous)
165                     get_fields = set(mapped.fields)
166                     exclude_fields = set(mapped.exclude).difference(get_fields)
167
168                     if 'absolute_uri' in get_fields:
169                         get_absolute_uri = True
170
171                     if not get_fields:
172                         get_fields = set([ f.attname.replace("_id", "", 1)
173                             for f in data._meta.fields ])
174
175                     # sets can be negated.
176                     for exclude in exclude_fields:
177                         if isinstance(exclude, basestring):
178                             get_fields.discard(exclude)
179
180                         elif isinstance(exclude, re._pattern_type):
181                             for field in get_fields.copy():
182                                 if exclude.match(field):
183                                     get_fields.discard(field)
184
185                 else:
186                     get_fields = set(fields)
187
188                 met_fields = self.method_fields(handler, get_fields)
189
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 ]]):
192                         if not f.rel:
193                             if f.attname in get_fields:
194                                 ret[f.attname] = _any(v(f))
195                                 get_fields.remove(f.attname)
196                         else:
197                             if f.attname[:-3] in get_fields:
198                                 ret[f.name] = _fk(data, f)
199                                 get_fields.remove(f.name)
200
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)
206
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)
212
213                         if inst:
214                             if hasattr(inst, 'all'):
215                                 ret[model] = _related(inst, fields)
216                             elif callable(inst):
217                                 if len(inspect.getargspec(inst)[0]) == 1:
218                                     ret[model] = _any(inst(), fields)
219                             else:
220                                 ret[model] = _model(inst, fields)
221
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))
227
228                     else:
229                         maybe = getattr(data, maybe_field, None)
230                         if maybe:
231                             if callable(maybe):
232                                 if len(inspect.getargspec(maybe)[0]) == 1:
233                                     ret[maybe_field] = _any(maybe())
234                             else:
235                                 ret[maybe_field] = _any(maybe)
236                         else:
237                             handler_f = getattr(handler or self.handler, maybe_field, None)
238
239                             if handler_f:
240                                 ret[maybe_field] = _any(handler_f(data))
241
242             else:
243                 for f in data._meta.fields:
244                     ret[f.attname] = _any(getattr(data, f.attname))
245
246                 fields = dir(data.__class__) + ret.keys()
247                 add_ons = [k for k in dir(data) if k not in fields]
248
249                 for k in add_ons:
250                     ret[k] = _any(getattr(data, k))
251
252             # resouce uri
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)
257
258                     try:
259                         ret['resource_uri'] = reverser( lambda: (url_id, fields) )()
260                     except NoReverseMatch, e:
261                         pass
262
263             if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
264                 try: ret['resource_uri'] = data.get_api_url()
265                 except: pass
266
267             # absolute uri
268             if hasattr(data, 'get_absolute_url') and get_absolute_uri:
269                 try: ret['absolute_uri'] = data.get_absolute_url()
270                 except: pass
271
272             return ret
273
274         def _qs(data, fields=()):
275             """
276             Querysets.
277             """
278             return [ _any(v, fields) for v in data ]
279
280         def _list(data):
281             """
282             Lists.
283             """
284             return [ _any(v) for v in data ]
285
286         def _dict(data):
287             """
288             Dictionaries.
289             """
290             return dict([ (k, _any(v)) for k, v in data.iteritems() ])
291
292         # Kickstart the seralizin'.
293         return _any(self.data, self.fields)
294
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:
298                 return klass
299
300     def render(self):
301         """
302         This super emitter does not implement `render`,
303         this is a job for the specific emitter below.
304         """
305         raise NotImplementedError("Please implement render.")
306
307     def stream_render(self, request, stream=True):
308         """
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.
313         """
314         yield self.render(request)
315
316     @classmethod
317     def get(cls, format):
318         """
319         Gets an emitter, returns the class and a content-type.
320         """
321         if cls.EMITTERS.has_key(format):
322             return cls.EMITTERS.get(format)
323
324         raise ValueError("No emitters found for type %s" % format)
325
326     @classmethod
327     def register(cls, name, klass, content_type='text/plain'):
328         """
329         Register an emitter.
330
331         Parameters::
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.
335         """
336         cls.EMITTERS[name] = (klass, content_type)
337
338     @classmethod
339     def unregister(cls, name):
340         """
341         Remove an emitter from the registry. Useful if you don't
342         want to provide output in one of the built-in emitters.
343         """
344         return cls.EMITTERS.pop(name, None)
345
346 class XMLEmitter(Emitter):
347     def _to_xml(self, xml, data):
348         if isinstance(data, (list, tuple)):
349             for item in data:
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)
357                 xml.endElement(key)
358         else:
359             xml.characters(smart_unicode(data))
360
361     def render(self, request):
362         stream = StringIO.StringIO()
363
364         xml = SimplerXMLGenerator(stream, "utf-8")
365         xml.startDocument()
366         xml.startElement("response", {})
367
368         self._to_xml(xml, self.construct())
369
370         xml.endElement("response")
371         xml.endDocument()
372
373         return stream.getvalue()
374
375 Emitter.register('xml', XMLEmitter, 'text/xml; charset=utf-8')
376 Mimer.register(lambda *a: None, ('text/xml',))
377
378 class JSONEmitter(Emitter):
379     """
380     JSON emitter, understands timestamps.
381     """
382     def render(self, request):
383         cb = request.GET.get('callback')
384         seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
385
386         # Callback
387         if cb:
388             return '%s(%s)' % (cb, seria)
389
390         return seria
391
392 Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')
393 Mimer.register(simplejson.loads, ('application/json',))
394
395 class YAMLEmitter(Emitter):
396     """
397     YAML emitter, uses `safe_dump` to omit the
398     specific types when outputting to non-Python.
399     """
400     def render(self, request):
401         return yaml.safe_dump(self.construct())
402
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',))
406
407 class PickleEmitter(Emitter):
408     """
409     Emitter that returns Python pickled.
410     """
411     def render(self, request):
412         return pickle.dumps(self.construct())
413
414 Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
415
416 """
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.
420
421 Read more: http://nadiana.com/python-pickle-insecure
422
423 Uncomment the line below to enable it. You're doing so at your own risk.
424 """
425 # Mimer.register(pickle.loads, ('application/python-pickle',))
426
427 class DjangoEmitter(Emitter):
428     """
429     Emitter for the Django serialized format.
430     """
431     def render(self, request, format='xml'):
432         if isinstance(self.data, HttpResponse):
433             return self.data
434         elif isinstance(self.data, (int, str)):
435             response = self.data
436         else:
437             response = serializers.serialize(format, self.data, indent=True)
438
439         return response
440
441 Emitter.register('django', DjangoEmitter, 'text/xml; charset=utf-8')