2 from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
3 from django.core.urlresolvers import reverse
4 from django.core.cache import cache
5 from django import get_version as django_version
6 from django.core.mail import send_mail, mail_admins
7 from django.conf import settings
8 from django.utils.translation import ugettext as _
9 from django.template import loader, TemplateDoesNotExist
10 from django.contrib.sites.models import Site
11 from decorator import decorator
13 from datetime import datetime, timedelta
15 __version__ = '0.2.3rc1'
20 def format_error(error):
21 return u"Piston/%s (Django %s) crash report:\n\n%s" % \
22 (get_version(), django_version(), error)
24 class rc_factory(object):
28 CODES = dict(ALL_OK = ('OK', 200),
29 CREATED = ('Created', 201),
30 DELETED = ('', 204), # 204 says "Don't send a body!"
31 BAD_REQUEST = ('Bad Request', 400),
32 FORBIDDEN = ('Forbidden', 401),
33 NOT_FOUND = ('Not Found', 404),
34 DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),
35 NOT_HERE = ('Gone', 410),
36 INTERNAL_ERROR = ('Internal Error', 500),
37 NOT_IMPLEMENTED = ('Not Implemented', 501),
38 THROTTLED = ('Throttled', 503))
40 def __getattr__(self, attr):
42 Returns a fresh `HttpResponse` when getting
43 an "attribute". This is backwards compatible
44 with 0.2, which is important.
47 (r, c) = self.CODES.get(attr)
49 raise AttributeError(attr)
51 return HttpResponse(r, content_type='text/plain', status=c)
55 class FormValidationError(Exception):
56 def __init__(self, form):
59 class HttpStatusCode(Exception):
60 def __init__(self, response):
61 self.response = response
63 def validate(v_form, operation='POST'):
65 def wrap(f, self, request, *a, **kwa):
66 form = v_form(getattr(request, operation))
69 return f(self, request, *a, **kwa)
71 raise FormValidationError(form)
74 def throttle(max_requests, timeout=60*60, extra=''):
76 Simple throttling decorator, caches
77 the amount of requests made in cache.
79 If used on a view where users are required to
80 log in, the username is used, otherwise the
81 IP address of the originating request is used.
84 - `max_requests`: The maximum number of requests
85 - `timeout`: The timeout for the cache entry (default: 1 hour)
88 def wrap(f, self, request, *args, **kwargs):
89 if request.user.is_authenticated():
90 ident = request.user.username
92 ident = request.META.get('REMOTE_ADDR', None)
94 if hasattr(request, 'throttle_extra'):
96 Since we want to be able to throttle on a per-
97 application basis, it's important that we realize
98 that `throttle_extra` might be set on the request
99 object. If so, append the identifier name with it.
101 ident += ':%s' % str(request.throttle_extra)
105 Preferrably we'd use incr/decr here, since they're
106 atomic in memcached, but it's in django-trunk so we
107 can't use it yet. If someone sees this after it's in
108 stable, you can change it here.
110 ident += ':%s' % extra
113 count, expiration = cache.get(ident, (1, None))
115 if expiration is None:
116 expiration = now + timeout
118 if count >= max_requests and expiration > now:
120 wait = int(expiration - now)
121 t.content = 'Throttled, wait %d seconds.' % wait
122 t['Retry-After'] = wait
125 cache.set(ident, (count+1, expiration), (expiration - now))
127 return f(self, request, *args, **kwargs)
130 def coerce_put_post(request):
132 Django doesn't particularly understand REST.
133 In case we send data over PUT, Django won't
134 actually look at the data and load it. We need
135 to twist its arm here.
137 The try/except abominiation here is due to a bug
138 in mod_python. This should fix it.
140 if request.method == "PUT":
142 request.method = "POST"
143 request._load_post_and_files()
144 request.method = "PUT"
145 except AttributeError:
146 request.META['REQUEST_METHOD'] = 'POST'
147 request._load_post_and_files()
148 request.META['REQUEST_METHOD'] = 'PUT'
150 request.PUT = request.POST
153 class MimerDataException(Exception):
155 Raised if the content_type and data don't match
162 def __init__(self, request):
163 self.request = request
165 def is_multipart(self):
166 content_type = self.content_type()
168 if content_type is not None:
169 return content_type.lstrip().startswith('multipart')
173 def loader_for_type(self, ctype):
175 Gets a function ref to deserialize content
176 for a certain mimetype.
178 for loadee, mimes in Mimer.TYPES.iteritems():
180 if ctype.startswith(mime):
183 def content_type(self):
185 Returns the content type of the request in all cases where it is
186 different than a submitted form - application/x-www-form-urlencoded
188 type_formencoded = "application/x-www-form-urlencoded"
190 ctype = self.request.META.get('CONTENT_TYPE', type_formencoded)
192 if type_formencoded in ctype:
199 Will look at the `Content-type` sent by the client, and maybe
200 deserialize the contents into the format they sent. This will
201 work for JSON, YAML, XML and Pickle. Since the data is not just
202 key-value (and maybe just a list), the data will be placed on
203 `request.data` instead, and the handler will have to read from
206 It will also set `request.content_type` so the handler has an easy
207 way to tell what's going on. `request.content_type` will always be
208 None for form-encoded and/or multipart form data (what your browser sends.)
210 ctype = self.content_type()
211 self.request.content_type = ctype
213 if not self.is_multipart() and ctype:
214 loadee = self.loader_for_type(ctype)
218 self.request.data = loadee(self.request.raw_post_data)
220 # Reset both POST and PUT from request, as its
221 # misleading having their presence around.
222 self.request.POST = self.request.PUT = dict()
223 except (TypeError, ValueError):
224 # This also catches if loadee is None.
225 raise MimerDataException
227 self.request.data = None
232 def register(cls, loadee, types):
233 cls.TYPES[loadee] = types
236 def unregister(cls, loadee):
237 return cls.TYPES.pop(loadee)
239 def translate_mime(request):
240 request = Mimer(request).translate()
242 def require_mime(*mimes):
244 Decorator requiring a certain mimetype. There's a nifty
245 helper called `require_extended` below which requires everything
246 we support except for post-data via form.
249 def wrap(f, self, request, *args, **kwargs):
253 rewrite = { 'json': 'application/json',
254 'yaml': 'application/x-yaml',
256 'pickle': 'application/python-pickle' }
258 for idx, mime in enumerate(mimes):
259 realmimes.add(rewrite.get(mime, mime))
261 if not m.content_type() in realmimes:
262 return rc.BAD_REQUEST
264 return f(self, request, *args, **kwargs)
267 require_extended = require_mime('json', 'yaml', 'xml', 'pickle')
269 def send_consumer_mail(consumer):
271 Send a consumer an email depending on what their status is.
274 subject = settings.PISTON_OAUTH_EMAIL_SUBJECTS[consumer.status]
275 except AttributeError:
276 subject = "Your API Consumer for %s " % Site.objects.get_current().name
277 if consumer.status == "accepted":
278 subject += "was accepted!"
279 elif consumer.status == "canceled":
280 subject += "has been canceled."
281 elif consumer.status == "rejected":
282 subject += "has been rejected."
284 subject += "is awaiting approval."
286 template = "piston/mails/consumer_%s.txt" % consumer.status
289 body = loader.render_to_string(template,
290 { 'consumer' : consumer, 'user' : consumer.user })
291 except TemplateDoesNotExist:
293 They haven't set up the templates, which means they might not want
299 sender = settings.PISTON_FROM_EMAIL
300 except AttributeError:
301 sender = settings.DEFAULT_FROM_EMAIL
304 send_mail(_(subject), body, sender, [consumer.user.email], fail_silently=True)
306 if consumer.status == 'pending' and len(settings.ADMINS):
307 mail_admins(_(subject), body, fail_silently=True)
309 if settings.DEBUG and consumer.user:
310 print "Mail being sent, to=%s" % consumer.user.email
311 print "Subject: %s" % _(subject)