Merge with upstream changes.
This commit is contained in:
Коммит
79ed439714
|
@ -0,0 +1,15 @@
|
|||
try:
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
except ImportError:
|
||||
# don't prevent use of paste if pkg_resources isn't installed
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
|
||||
try:
|
||||
import modulefinder
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
for p in __path__:
|
||||
modulefinder.AddPackagePath(__name__, p)
|
|
@ -13,7 +13,7 @@ def generate_doc(handler_cls):
|
|||
for the given handler. Use this to generate
|
||||
documentation for your API.
|
||||
"""
|
||||
if not type(handler_cls) is handler.HandlerMetaClass:
|
||||
if isinstance(type(handler_cls), handler.HandlerMetaClass):
|
||||
raise ValueError("Give me handler, not %s" % type(handler_cls))
|
||||
|
||||
return HandlerDocumentation(handler_cls)
|
||||
|
@ -89,7 +89,7 @@ class HandlerDocumentation(object):
|
|||
if not met:
|
||||
continue
|
||||
|
||||
stale = inspect.getmodule(met) is handler
|
||||
stale = inspect.getmodule(met.im_func) is not inspect.getmodule(self.handler)
|
||||
|
||||
if not self.handler.is_anonymous:
|
||||
if met and (not stale or include_default):
|
||||
|
|
|
@ -59,7 +59,7 @@ class Emitter(object):
|
|||
as the methods on the handler. Issue58 says that's no good.
|
||||
"""
|
||||
EMITTERS = { }
|
||||
RESERVED_FIELDS = set([ 'read', 'update', 'create',
|
||||
RESERVED_FIELDS = set([ 'read', 'update', 'create',
|
||||
'delete', 'model', 'anonymous',
|
||||
'allowed_methods', 'fields', 'exclude' ])
|
||||
|
||||
|
@ -69,16 +69,16 @@ class Emitter(object):
|
|||
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)
|
||||
|
||||
|
@ -86,13 +86,13 @@ class Emitter(object):
|
|||
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=()):
|
||||
|
@ -100,13 +100,13 @@ class Emitter(object):
|
|||
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, (tuple, list, set)):
|
||||
ret = _list(thing, fields=fields)
|
||||
elif isinstance(thing, dict):
|
||||
ret = _dict(thing)
|
||||
ret = _dict(thing, fields)
|
||||
elif isinstance(thing, decimal.Decimal):
|
||||
ret = str(thing)
|
||||
elif isinstance(thing, Model):
|
||||
|
@ -132,19 +132,19 @@ class Emitter(object):
|
|||
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
|
||||
|
@ -153,7 +153,7 @@ class Emitter(object):
|
|||
ret = { }
|
||||
handler = self.in_typemapper(type(data), self.anonymous)
|
||||
get_absolute_uri = False
|
||||
|
||||
|
||||
if handler or fields:
|
||||
v = lambda f: getattr(data, f.attname)
|
||||
|
||||
|
@ -168,27 +168,30 @@ class Emitter(object):
|
|||
|
||||
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 ])
|
||||
|
||||
for f in data._meta.fields + data._meta.virtual_fields])
|
||||
|
||||
if hasattr(mapped, 'extra_fields'):
|
||||
get_fields.update(mapped.extra_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:
|
||||
|
||||
for f in data._meta.local_fields + data._meta.virtual_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:
|
||||
|
@ -198,13 +201,13 @@ class Emitter(object):
|
|||
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)):
|
||||
|
@ -226,7 +229,7 @@ class Emitter(object):
|
|||
# using different names.
|
||||
ret[maybe_field] = _any(met_fields[maybe_field](data))
|
||||
|
||||
else:
|
||||
else:
|
||||
maybe = getattr(data, maybe_field, None)
|
||||
if maybe:
|
||||
if callable(maybe):
|
||||
|
@ -243,13 +246,13 @@ class Emitter(object):
|
|||
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)
|
||||
|
@ -260,51 +263,51 @@ class Emitter(object):
|
|||
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):
|
||||
|
||||
def _list(data, fields=()):
|
||||
"""
|
||||
Lists.
|
||||
"""
|
||||
return [ _any(v) for v in data ]
|
||||
|
||||
def _dict(data):
|
||||
return [ _any(v, fields) for v in data ]
|
||||
|
||||
def _dict(data, fields):
|
||||
"""
|
||||
Dictionaries.
|
||||
"""
|
||||
return dict([ (k, _any(v)) for k, v in data.iteritems() ])
|
||||
|
||||
return dict([ (k, _any(v, fields)) 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
|
||||
|
@ -313,7 +316,7 @@ class Emitter(object):
|
|||
more memory friendly for large datasets.
|
||||
"""
|
||||
yield self.render(request)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get(cls, format):
|
||||
"""
|
||||
|
@ -323,19 +326,19 @@ class Emitter(object):
|
|||
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):
|
||||
"""
|
||||
|
@ -343,7 +346,7 @@ class Emitter(object):
|
|||
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)):
|
||||
|
@ -361,16 +364,16 @@ class XMLEmitter(Emitter):
|
|||
|
||||
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')
|
||||
|
@ -389,10 +392,10 @@ class JSONEmitter(Emitter):
|
|||
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
|
||||
|
@ -411,7 +414,7 @@ class PickleEmitter(Emitter):
|
|||
"""
|
||||
def render(self, request):
|
||||
return pickle.dumps(self.construct())
|
||||
|
||||
|
||||
Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
|
||||
|
||||
"""
|
||||
|
@ -438,5 +441,5 @@ class DjangoEmitter(Emitter):
|
|||
response = serializers.serialize(format, self.data, indent=True)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
Emitter.register('django', DjangoEmitter, 'text/xml; charset=utf-8')
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
from piston.doc import generate_doc
|
||||
from piston.handler import handler_tracker
|
||||
import re
|
||||
|
||||
def generate_piston_documentation(app, docname, source):
|
||||
e = re.compile(r"^\.\. piston_handlers:: ([\w\.]+)$")
|
||||
old_source = source[0].split("\n")
|
||||
new_source = old_source[:]
|
||||
for line_nr, line in enumerate(old_source):
|
||||
m = e.match(line)
|
||||
if m:
|
||||
module = m.groups()[0]
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
new_lines = []
|
||||
for handler in handler_tracker:
|
||||
doc = generate_doc(handler)
|
||||
new_lines.append(doc.name)
|
||||
new_lines.append("-" * len(doc.name))
|
||||
new_lines.append('::\n')
|
||||
new_lines.append('\t' + doc.get_resource_uri_template() + '\n')
|
||||
new_lines.append('Accepted methods:')
|
||||
for method in doc.allowed_methods:
|
||||
new_lines.append('\t* ' + method)
|
||||
new_lines.append('')
|
||||
if doc.doc:
|
||||
new_lines.append(doc.doc)
|
||||
new_source[line_nr:line_nr+1] = new_lines
|
||||
|
||||
source[0] = "\n".join(new_source)
|
||||
return source
|
||||
|
||||
def setup(app):
|
||||
app.connect('source-read', generate_piston_documentation)
|
|
@ -166,11 +166,20 @@ class Resource(object):
|
|||
result = self.error_handler(e, request, meth)
|
||||
|
||||
|
||||
emitter, ct = Emitter.get(em_format)
|
||||
fields = handler.fields
|
||||
if hasattr(handler, 'list_fields') and (
|
||||
isinstance(result, list) or isinstance(result, QuerySet)):
|
||||
fields = handler.list_fields
|
||||
try:
|
||||
emitter, ct = Emitter.get(em_format)
|
||||
except ValueError:
|
||||
result = rc.BAD_REQUEST
|
||||
result.content = "Invalid output format specified '%s'." % em_format
|
||||
return result
|
||||
|
||||
try:
|
||||
result, fields = result
|
||||
except ValueError:
|
||||
fields = handler.fields
|
||||
if hasattr(handler, 'list_fields') and (
|
||||
isinstance(result, list) or isinstance(result, QuerySet)):
|
||||
fields = handler.list_fields
|
||||
|
||||
status_code = 200
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from django.core import mail
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from django.template import loader, TemplateDoesNotExist
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils import simplejson
|
||||
|
||||
|
@ -22,9 +23,28 @@ class ConsumerTest(TestCase):
|
|||
self.consumer.user = User.objects.get(pk=3)
|
||||
self.consumer.generate_random_codes()
|
||||
|
||||
def _pre_test_email(self):
|
||||
template = "piston/mails/consumer_%s.txt" % self.consumer.status
|
||||
try:
|
||||
loader.render_to_string(template, {
|
||||
'consumer': self.consumer,
|
||||
'user': self.consumer.user
|
||||
})
|
||||
return True
|
||||
except TemplateDoesNotExist:
|
||||
"""
|
||||
They haven't set up the templates, which means they might not want
|
||||
these emails sent.
|
||||
"""
|
||||
return False
|
||||
|
||||
def test_create_pending(self):
|
||||
""" Ensure creating a pending Consumer sends proper emails """
|
||||
# If it's pending we should have two messages in the outbox; one
|
||||
# Verify if the emails can be sent
|
||||
if not self._pre_test_email():
|
||||
return
|
||||
|
||||
# If it's pending we should have two messages in the outbox; one
|
||||
# to the consumer and one to the site admins.
|
||||
if len(settings.ADMINS):
|
||||
self.assertEquals(len(mail.outbox), 2)
|
||||
|
@ -41,8 +61,12 @@ class ConsumerTest(TestCase):
|
|||
mail.outbox = []
|
||||
|
||||
# Delete the consumer, which should fire off the cancel email.
|
||||
self.consumer.delete()
|
||||
|
||||
self.consumer.delete()
|
||||
|
||||
# Verify if the emails can be sent
|
||||
if not self._pre_test_email():
|
||||
return
|
||||
|
||||
self.assertEquals(len(mail.outbox), 1)
|
||||
expected = "Your API Consumer for example.com has been canceled."
|
||||
self.assertEquals(mail.outbox[0].subject, expected)
|
||||
|
@ -172,4 +196,3 @@ class ErrorHandlerTest(TestCase):
|
|||
|
||||
self.assertTrue(isinstance(response, HttpResponse), "Expected a response, not: %s"
|
||||
% response)
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -20,6 +20,7 @@ setup(
|
|||
author = 'Jesper Noehr',
|
||||
author_email = 'jesper@noehr.org',
|
||||
packages = find_packages(),
|
||||
namespace_packages = ['piston'],
|
||||
include_package_data = True,
|
||||
zip_safe = False,
|
||||
classifiers = [
|
||||
|
|
Загрузка…
Ссылка в новой задаче