Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RESTful APIs with Tastypie

RESTful APIs with Tastypie

PyCon 2012 talk - An overview of Tastypie, a REST framework for Django.

daniellindsley

March 11, 2012
Tweet

More Decks by daniellindsley

Other Decks in Technology

Transcript

  1. Who am I? • Daniel Lindsley • Consulting & OSS

    as Toast Driven • Primary author of Tastypie
  2. What is Tastypie? • A REST framework for Django •

    Designed for extension • Supports both Model & non-Model data
  3. What is Tastypie? • A REST framework for Django •

    Designed for extension • Supports both Model & non-Model data • http://tastypieapi.org/
  4. Make good use of HTTP Try to be “of the

    internet” & use the REST methods/status codes properly
  5. HATEOAS • “Hypermedia As The Engine Of Application State” •

    Basically, the user shouldn’t have to know anything in advance • All about explore-ability • Deep linking • http://en.wikipedia.org/wiki/HATEOAS
  6. Tastypie • Builds on top of Django & plays nicely

    with other apps • Full GET/POST/PUT/DELETE/PATCH • Any data source (not just Models) • Designed to be extended
  7. Tastypie (cont.) • Supports a variety of serialization formats •

    JSON • XML • YAML • bplist • Easy to add more
  8. Tastypie (cont.) • HATEOAS by default (you’ll see soon) •

    Lots of hooks for customization • Well-tested • Decently documented
  9. High-Level 1. Code goes in our apps, not Django 2.

    Define the resource for User 3. Hook up that resource in your URLconf
  10. 2a. Setup # Assuming we’re in your project directory... $

    cd <myapp> # Substitute your app_name here. $ mkdir api $ touch api/__init__.py $ touch api/resources.py # Done!
  11. 2b. User Resource from django.contrib.auth.models import User from tastypie.resources import

    ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all()
  12. 3. URLconf # In your ``ROOT_URLCONF``... # Stuff then... from

    tastypie.api import Api from <myapp>.api.resources import UserResource v1_api = Api() v1_api.register(UserResource()) urlpatterns = patterns(‘’, (r’^api/’, include(v1_api.urls), # Then the usual... )
  13. What’s there? • /api/v1/ - A list of all available

    resources • /api/v1/user/ - A list of all users • /api/v1/user/2/ - A specific user • /api/v1/user/schema/ - A definition of what an individual user consists of • /api/v1/user/multiple/1;4;5/ - Get those three users as one request
  14. What’s there? (cont.) • All serialization formats available* • curl

    -H “Accept: application/xml” http://localhost:8000/api/v1/ user/ • http://localhost:8000/api/v1/user/2/? format=yaml * Provided lxml, PyYAML, biplist are installed.
  15. What’s there? (cont.) • Serialization format negotiated by either Accepts

    header or the ”? format=json” GET param • Pagination by default • Everyone has full read-only GET access
  16. What’s not there? (Yet) • Leaking sensitive information! • email/password/is_staff/is_superuser

    • Ability to filter • Authentication / Authorization • Caching (disabled by default)
  17. What’s not there? (Yet) • Leaking sensitive information! • email/password/is_staff/is_superuser

    • Ability to filter • Authentication / Authorization • Caching (disabled by default) • Throttling (disabled by default)
  18. Fix data leaks from django.contrib.auth.models import User from tastypie.resources import

    ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all() excludes = [‘email’, ‘password’, ‘is_staff’, ‘is_superuser’]
  19. Add BASIC Auth from django.contrib.auth.models import User from tastypie.authentication import

    BasicAuthentication from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: # What was there before... authentication = BasicAuthentication()
  20. Add filtering from django.contrib.auth.models import User from tastypie.authentication import BasicAuthentication

    from tastypie.resources import ModelResource, ALL class UserResource(ModelResource): class Meta: # What was there before... filtering = { ‘username’: ALL, ‘date_joined’: [‘range’, ‘gt’, ‘gte’, ‘lt’, ‘lte’], }
  21. Filtering • Using GET params, we can now filter out

    what we want. • Examples: • curl http://localhost:8000/api/v1/user/? username__startswith=a • curl http://localhost:8000/api/v1/user/? date_joined__gte=2011-12-01
  22. Add authorization from django.contrib.auth.models import User from tastypie.authentication import BasicAuthentication

    from tastypie.authorization import DjangoAuthorization from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: # What was there before... authorization = DjangoAuthorization()
  23. Add caching* from django.contrib.auth.models import User from tastypie.authentication import BasicAuthentication

    from tastypie.authorization import DjangoAuthorization from tastypie.cache import SimpleCache from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: # What was there before... cache = SimpleCache() * We’ll talk more about caching later.
  24. Add throttling from django.contrib.auth.models import User from tastypie.authentication import BasicAuthentication

    from tastypie.authorization import DjangoAuthorization from tastypie.cache import SimpleCache from tastypie.resources import ModelResource from tastypie.throttle import CacheDBThrottle class UserResource(ModelResource): class Meta: # What was there before... throttle = CacheDBThrottle()
  25. What’s there now? • Everything we had before • Full

    GET/POST/PUT/DELETE/PATCH access • Only registered users can use the API & only perform actions on objects they’re allowed to
  26. What’s there now? (cont.) • Object-level caching (GET detail) •

    Logged throttling that limits users to 150 reqs per hour
  27. What’s there now? (cont.) • Object-level caching (GET detail) •

    Logged throttling that limits users to 150 reqs per hour • The ability to filter the content
  28. Extensibility • Tastypie tries to use reasonable defaults: • You

    probably want JSON • You probably want full POST/PUT/ DELETE by default • You probably want to use the Model’s default manager unfiltered
  29. Extensibility • Plug in custom classes/instances for things like: •

    Serialization • Authentication • Authorization
  30. Extensibility • Plug in custom classes/instances for things like: •

    Serialization • Authentication • Authorization • Pagination
  31. Extensibility • Plug in custom classes/instances for things like: •

    Serialization • Authentication • Authorization • Pagination • Caching
  32. Extensibility • Plug in custom classes/instances for things like: •

    Serialization • Authentication • Authorization • Pagination • Caching • Throttling
  33. Extensibility • Resource has lots of methods, many of which

    are pretty granular • Override or extend as meets your needs
  34. Customize serialization • As an example, let’s customize serialization •

    Supports JSON, XML, YAML, bplist by default • Let’s disable everything but JSON & XML, then add a custom type
  35. Just JSON & XML, please from django.contrib.auth.models import User from

    tastypie.resources import ModelResource from tastypie.serialization import Serializer class UserResource(ModelResource): class Meta: queryset = User.objects.all() excludes = [‘email’, ‘password’, ‘is_staff’, ‘is_superuser’] serializer = Serializer(formats=[‘json’, ‘xml’])
  36. HTML serialization from django.shortcuts import render_to_response from tastypie.serialization import Serializer

    class TemplateSerializer(Serializer): formats = Serializer.formats + [‘html’] def to_html(self, data): template_name = ‘api/api_detail.html’ if ‘objects’ in data: template_name = ‘api/api_list.html’ return render_to_response(template_name, data)
  37. HTML serialization (cont.) # ಠ_ಠ import cgi from stringio import

    StringIO class TemplateSerializer(Serializer): # ... def from_html(self, content): form = cgi.FieldStorage(fp=StringIO(content)) data = {} for key in form: data[key] = form[key].value return data
  38. Now use it from django.contrib.auth.models import User from tastypie.resources import

    ModelResource from myapp.api.serializers import TemplateSerializer class UserResource(ModelResource): class Meta: queryset = User.objects.all() excludes = [‘email’, ‘password’, ‘is_staff’, ‘is_superuser’] serializer = TemplateSerializer(formats=[‘json’, ‘xml’, ‘html’])
  39. Fields • You haven’t seen it yet, but just like

    a ModelForm, you can control all the exposed fields on a Resource/ ModelResource
  40. Fields • You haven’t seen it yet, but just like

    a ModelForm, you can control all the exposed fields on a Resource/ ModelResource • Just like Django, you use a declarative syntax
  41. Fields from django.contrib.auth.models import User from tastypie import fields from

    tastypie.resources import ModelResource class UserResource(ModelResource): # Provided they take no args, even callables work! full_name = fields.CharField(‘get_full_name’, blank=True) class Meta: queryset = User.objects.all() excludes = [‘email’, ‘password’, ‘is_staff’, ‘is_superuser’]
  42. Fields (cont.) • Also like ModelForm, you can control how

    data gets prepared for presentation (dehydrate) or accepted from the user (hydrate)
  43. Fields (cont.) • Also like ModelForm, you can control how

    data gets prepared for presentation (dehydrate) or accepted from the user (hydrate) • Happens automatically on fields with attribute=... set
  44. Fields (cont.) • Also like ModelForm, you can control how

    data gets prepared for presentation (dehydrate) or accepted from the user (hydrate) • Happens automatically on fields with attribute=... set • Can provide methods for non-simple access
  45. Fields (cont.) class UserResource(ModelResource): # Assuming all the same bits

    as before. full_name = fields.CharField(blank=True) def dehydrate_full_name(self, bundle): return bundle.obj.get_full_name() def hydrate_full_name(self, bundle): name_bits = bundle.data.get(‘full_name’,’’).split() bundle.obj.first_name = name_bits[0] bundle.obj.last_name = ‘ ‘.join(name_bits[1:]) return bundle
  46. Fields (cont.) • This just scratches the surface of what

    dehydrate/hydrate can do • ModelResource uses introspection & just creates the fields for you
  47. Caching • The SimpleCache combined with Resource.cached_obj_get caches SINGLE objects

    only! • Doesn’t cache the serialized output • Doesn’t cache the list view
  48. Why? • More complex behaviors get opinionated fast • Tastypie

    would rather be general & give you the tools to build what you need
  49. Why? • More complex behaviors get opinionated fast • Tastypie

    would rather be general & give you the tools to build what you need • Filters & serialization formats make it complex
  50. Why? • More complex behaviors get opinionated fast • Tastypie

    would rather be general & give you the tools to build what you need • Filters & serialization formats make it complex • Besides...
  51. Varnish! • https://www.varnish-cache.org/ • Super-fast caching reverse proxy in C

    • Already caches by URI/headers • Way faster than the Django request/ response cycle
  52. Varnish! • https://www.varnish-cache.org/ • Super-fast caching reverse proxy in C

    • Already caches by URI/headers • Way faster than the Django request/ response cycle • POST/PUT/DELETE just pass through
  53. Varnish! (cont.) • So put Varnish in front of your

    API (& perhaps the rest of your site) & win in the general case
  54. Varnish! (cont.) • So put Varnish in front of your

    API (& perhaps the rest of your site) & win in the general case • Additionally, use Tastypie’s internal caching to further speed up Varnish cache-misses
  55. Varnish! (cont.) • So put Varnish in front of your

    API (& perhaps the rest of your site) & win in the general case • Additionally, use Tastypie’s internal caching to further speed up Varnish cache-misses • Easy to extend Resource to add in more caching
  56. Varnish! (cont.) • So put Varnish in front of your

    API (& perhaps the rest of your site) & win in the general case • Additionally, use Tastypie’s internal caching to further speed up Varnish cache-misses • Easy to extend Resource to add in more caching • If you get to that point, you’re already serving way more load than I ever have
  57. Source dive? • If you hit up tastypie/resources.py, you’ll find

    something interesting • ModelResource is just a relatively thin (~300 lines) wrapper on top of Resource (~1200 lines)
  58. Source dive? • If you hit up tastypie/resources.py, you’ll find

    something interesting • ModelResource is just a relatively thin (~300 lines) wrapper on top of Resource (~1200 lines) • Just the ORM/Model bits
  59. The Mighty Resource • By subclassing from Resource & overriding

    at least 3 (up to 9 for CRUD) methods, you can hook up any data source • For giggles, let’s hook up Solr! • For brevity, we’ll do GET-only
  60. SolrResource import pysolr from tastypie import fields from tastypie.resources import

    Resource # Wrap the response dict to be object-like. class SolrObject(object): def __init__(self, initial=None): self.__dict__[‘_data’] = initial or {} def __getattr__(self, key): return self._data.get(key, None)
  61. SolrResource (cont.) class SolrResource(Resource): # ... def get_resource_uri(self, bundle_or_obj): #

    Super-naïve. return ‘/’.join([self._meta.api_name, self._meta.resource_name, bundle_or_obj.id]) def get_object_list(self, request, **kwargs): query = kwargs.get(‘query’, None) or request.GET.get(‘q’, ‘*:*’) solr = pysolr.Solr(‘http://localhost:8963/solr’) return [SolrObject(initial=res) for res in solr.search(query)])
  62. SolrResource (cont.) class SolrResource(Resource): # ... def obj_get_list(self, request=None, **kwargs):

    return self.get_object_list(request) def obj_get(self, request=None, **kwargs): return self.get_object_list(request, {‘query’: ‘id:%s’ % kwargs[‘pk’])
  63. The Mighty Resource • Takes some work but still does

    a lot for you • Docs have a more complete example based on Riak • See also django-tastypie-nonrel
  64. Piecrust • Seems like an awesome, helpful idea. Let’s do

    it! • Late 2011, I tried extracting Tastypie to work everywhere • Tastypie would just become a light shim on top with Django conveniences • https://github.com/toastdriven/piecrust
  65. Piecrust • Finished the extraction, not the tests/ docs •

    Close to functional, but... • Failed in terms of complexity & lack of standardization
  66. Piecrust • Complex areas: • Non-standard request/response objects • Non-standard

    URL setups • Data storage layer • Relying on Django makes these easy(- ier)
  67. Piecrust • Just implementing for Flask took a couple hundred

    lines alone & it was incomplete • Paradox of choice • Code is there if you want to play