This commit is contained in:
Ionel Cristian Maries 2010-06-25 12:12:10 +03:00
Родитель 4b56dc3ae7 ae9cbfc223
Коммит 79ed439714
7 изменённых файлов: 153 добавлений и 65 удалений

Просмотреть файл

@ -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')

37
piston/handlers_doc.py Normal file
Просмотреть файл

@ -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)

Просмотреть файл

@ -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 = [