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
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
2b. User Resource from django.contrib.auth.models import User from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all()
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... )
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
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
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’]
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()
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’], }
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
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()
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.
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()
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
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
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
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
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’])
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
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’])
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
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’]
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
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
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
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
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...
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
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
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
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
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
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)
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
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
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)
SolrResource (cont.) class SolrResource(Resource): id = fields.CharField(attribute=’id’) title = fields.CharField(attribute=‘text’) class Meta: resource_name = ‘solr’ object_class = SolrObject
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