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. RESTful APIs with
    Tastypie

    View Slide

  2. ...or...

    View Slide

  3. How I learned to stop
    worrying and love
    the JSON

    View Slide

  4. Who am I?
    • Daniel Lindsley

    View Slide

  5. Who am I?
    • Daniel Lindsley
    • Consulting & OSS as Toast Driven

    View Slide

  6. Who am I?
    • Daniel Lindsley
    • Consulting & OSS as Toast Driven
    • Primary author of Tastypie

    View Slide

  7. What is Tastypie?
    • A REST framework for Django

    View Slide

  8. What is Tastypie?
    • A REST framework for Django
    • Designed for extension

    View Slide

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

    View Slide

  10. What is Tastypie?
    • A REST framework for Django
    • Designed for extension
    • Supports both Model & non-Model
    data
    • http://tastypieapi.org/

    View Slide

  11. Philosophy

    View Slide

  12. Make good use
    of HTTP

    View Slide

  13. Make good use
    of HTTP
    Try to be “of the internet” & use the REST
    methods/status codes properly

    View Slide

  14. Graceful
    Degradation

    View Slide

  15. Graceful
    Degradation
    Try to keep backward compatibility & give
    users a gradual upgrade path

    View Slide

  16. Flexible serialization

    View Slide

  17. Flexible serialization
    Not everyone wants JSON

    View Slide

  18. Wait. Scratch that.

    View Slide

  19. Flexible serialization
    Not everyone wants JSON

    View Slide

  20. Flexible
    EVERYTHING

    View Slide

  21. Flexible
    EVERYTHING
    Customizability is a core feature

    View Slide

  22. Data can round-trip

    View Slide

  23. Data can round-trip
    Anything you can GET,
    you should be able to POST/PUT

    View Slide

  24. Reasonable defaults

    View Slide

  25. Reasonable defaults
    But easy to extend

    View Slide

  26. URIs everywhere!

    View Slide

  27. URIs everywhere!
    Make HATEOAS a reality

    View Slide

  28. HATEOAS?

    View Slide

  29. Gesundheit!

    View Slide

  30. 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

    View Slide

  31. So what about
    Tastypie?

    View Slide

  32. Pie?

    View Slide

  33. Pie?
    Pie?
    Pie?
    Pie?
    Pie?
    Pie?
    Pie?
    Pie?

    View Slide

  34. He can’t haz.

    View Slide

  35. He can’t haz.
    But you can!

    View Slide

  36. 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

    View Slide

  37. Tastypie (cont.)
    • Supports a variety of serialization formats
    • JSON
    • XML
    • YAML
    • bplist
    • Easy to add more

    View Slide

  38. Tastypie (cont.)
    • HATEOAS by default (you’ll see soon)
    • Lots of hooks for customization
    • Well-tested
    • Decently documented

    View Slide

  39. The Setup

    View Slide

  40. Installation
    pip install django-tastypie

    View Slide

  41. Installation (cont.)
    INSTALLED_APPS += [‘tastypie’]

    View Slide

  42. Installation (cont.)
    $ ./manage.py syncdb

    View Slide

  43. Done.

    View Slide

  44. Let’s add an API for
    django.contrib.auth

    View Slide

  45. High-Level
    1. Code goes in our apps, not Django

    View Slide

  46. High-Level
    1. Code goes in our apps, not Django
    2. Define the resource for User

    View Slide

  47. High-Level
    1. Code goes in our apps, not Django
    2. Define the resource for User
    3. Hook up that resource in your URLconf

    View Slide

  48. 1. Don’t fork Django!
    No, seriously. You don’t need to. Done.

    View Slide

  49. 2a. Setup
    # Assuming we’re in your project directory...
    $ cd # Substitute your app_name here.
    $ mkdir api
    $ touch api/__init__.py
    $ touch api/resources.py
    # Done!

    View Slide

  50. 2b. User Resource
    from django.contrib.auth.models import User
    from tastypie.resources import ModelResource
    class UserResource(ModelResource):
    class Meta:
    queryset = User.objects.all()

    View Slide

  51. 3. URLconf
    # In your ``ROOT_URLCONF``...
    # Stuff then...
    from tastypie.api import Api
    from .api.resources import UserResource
    v1_api = Api()
    v1_api.register(UserResource())
    urlpatterns = patterns(‘’,
    (r’^api/’, include(v1_api.urls),
    # Then the usual...
    )

    View Slide

  52. HTTP 201

    View Slide

  53. Hit your new API.
    Curl: http://localhost:8000/api/v1/
    Browser: http://localhost:8000/api/v1/?format=json

    View Slide

  54. Demo-time.

    View Slide

  55. 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

    View Slide

  56. 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.

    View Slide

  57. 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

    View Slide

  58. What’s not there? (Yet)
    • Leaking sensitive information!
    • email/password/is_staff/is_superuser

    View Slide

  59. What’s not there? (Yet)
    • Leaking sensitive information!
    • email/password/is_staff/is_superuser
    • Ability to filter

    View Slide

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

    View Slide

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

    View Slide

  62. 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)

    View Slide

  63. 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’]

    View Slide

  64. Authentication
    “Are you a recognized user?”

    View Slide

  65. 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()

    View Slide

  66. 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’],
    }

    View Slide

  67. 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

    View Slide

  68. Authorization
    “Are you allowed to perform that action?”

    View Slide

  69. 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()

    View Slide

  70. 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.

    View Slide

  71. 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()

    View Slide

  72. Whew.

    View Slide

  73. What’s there now?
    • Everything we had before

    View Slide

  74. What’s there now?
    • Everything we had before
    • Full GET/POST/PUT/DELETE/PATCH access

    View Slide

  75. 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

    View Slide

  76. What’s there now? (cont.)
    • Object-level caching (GET detail)

    View Slide

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

    View Slide

  78. 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

    View Slide

  79. HTTP 302

    View Slide

  80. Extensibility

    View Slide

  81. Why classes?

    View Slide

  82. Why classes?
    Not because I’m OO-crazy.
    It makes extending behavior trivial.

    View Slide

  83. Why so many
    classes?

    View Slide

  84. Why so many
    classes?
    Composition > Inheritance

    View Slide

  85. Why so many
    methods?

    View Slide

  86. Why so many
    methods?
    Hooks, hooks, hooks.
    Also makes delegating to composition behaviors easy.

    View Slide

  87. 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

    View Slide

  88. But.

    View Slide

  89. YMMV, so let’s make
    that possible.

    View Slide

  90. Extensibility
    • Plug in custom classes/instances for things like:
    • Serialization

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  96. Extensibility
    • Resource has lots of methods, many of
    which are pretty granular
    • Override or extend as meets your
    needs

    View Slide

  97. Customize serialization
    • As an example, let’s customize
    serialization

    View Slide

  98. Customize serialization
    • As an example, let’s customize
    serialization
    • Supports JSON, XML, YAML, bplist by
    default

    View Slide

  99. 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

    View Slide

  100. 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’])

    View Slide

  101. Hm, now what
    format is missing?

    View Slide

  102. HTML

    View Slide

  103. 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)

    View Slide

  104. 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

    View Slide

  105. 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’])

    View Slide

  106. Fields

    View Slide

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

    View Slide

  108. 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

    View Slide

  109. 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’]

    View Slide

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

    View Slide

  111. 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

    View Slide

  112. 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

    View Slide

  113. 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

    View Slide

  114. Fields (cont.)
    • This just scratches the surface of what
    dehydrate/hydrate can do
    • ModelResource uses introspection & just
    creates the fields for you

    View Slide

  115. Caching

    View Slide

  116. Caching
    • The SimpleCache combined with
    Resource.cached_obj_get caches
    SINGLE objects only!

    View Slide

  117. Caching
    • The SimpleCache combined with
    Resource.cached_obj_get caches
    SINGLE objects only!
    • Doesn’t cache the serialized output

    View Slide

  118. 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

    View Slide

  119. Why?
    • More complex behaviors get opinionated fast

    View Slide

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

    View Slide

  121. 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

    View Slide

  122. 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...

    View Slide

  123. What you actually
    want is Varnish.

    View Slide

  124. Varnish!
    • https://www.varnish-cache.org/

    View Slide

  125. Varnish!
    • https://www.varnish-cache.org/
    • Super-fast caching reverse proxy in C

    View Slide

  126. Varnish!
    • https://www.varnish-cache.org/
    • Super-fast caching reverse proxy in C
    • Already caches by URI/headers

    View Slide

  127. 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

    View Slide

  128. 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

    View Slide

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

    View Slide

  130. 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

    View Slide

  131. 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

    View Slide

  132. 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

    View Slide

  133. Long story short:
    You should be using Varnish.

    View Slide

  134. Data source
    Not Just Models!

    View Slide

  135. Source dive?
    • If you hit up tastypie/resources.py,
    you’ll find something interesting

    View Slide

  136. 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)

    View Slide

  137. 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

    View Slide

  138. That’s right, virtually
    everything in Tastypie is
    available to non-ORM
    setups.

    View Slide

  139. 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

    View Slide

  140. 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)

    View Slide

  141. SolrResource (cont.)
    class SolrResource(Resource):
    id = fields.CharField(attribute=’id’)
    title = fields.CharField(attribute=‘text’)
    class Meta:
    resource_name = ‘solr’
    object_class = SolrObject

    View Slide

  142. 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)])

    View Slide

  143. 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’])

    View Slide

  144. 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

    View Slide

  145. HTTP 418

    View Slide

  146. Now, let’s talk about
    a personal failure.

    View Slide

  147. Man, this seems
    super-useful.
    Wish I could use it with...

    View Slide

  148. Man, this seems
    super-useful.
    Wish I could use it with...
    Flask

    View Slide

  149. Man, this seems
    super-useful.
    Wish I could use it with...
    Flask
    Pyramid

    View Slide

  150. Man, this seems
    super-useful.
    Wish I could use it with...
    Flask
    Pyramid
    Itty

    View Slide

  151. Man, this seems
    super-useful.
    Wish I could use it with...
    Flask
    Pyramid
    Itty
    Straight WSGI

    View Slide

  152. Piecrust
    The extraction that failed

    View Slide

  153. 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

    View Slide

  154. Piecrust
    • Finished the extraction, not the tests/
    docs

    View Slide

  155. Piecrust
    • Finished the extraction, not the tests/
    docs
    • Close to functional, but...

    View Slide

  156. Piecrust
    • Finished the extraction, not the tests/
    docs
    • Close to functional, but...
    • Failed in terms of complexity & lack of
    standardization

    View Slide

  157. Piecrust
    • Complex areas:
    • Non-standard request/response
    objects
    • Non-standard URL setups
    • Data storage layer
    • Relying on Django makes these easy(-
    ier)

    View Slide

  158. 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

    View Slide

  159. Thanks!

    View Slide

  160. Images in main theme
    by Julia Elman
    @juliaelman
    http://juliaelman.com/

    View Slide

  161. I’m Daniel Lindsley
    of Toast Driven
    @toastdriven
    http://toastdriven.com/

    View Slide