Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save resalisbury/f409f2b1d51028c186e9b6eb9844917f to your computer and use it in GitHub Desktop.
Save resalisbury/f409f2b1d51028c186e9b6eb9844917f to your computer and use it in GitHub Desktop.
Presentation
"""
____ ____ _____
| _ \| _ \| ___|
| | | | |_) | |
| |_| | _ <| _|
|____/|_| \_\_|
_____ _ _____ _ _ ___ _ _ _
|_ _(_)_ __ ___ |_ _| __(_) ___| | _____ ( _ ) | | | | __ _ ___| | _____
| | | | '_ \/ __| | || '__| |/ __| |/ / __| / _ \/\ | |_| |/ _` |/ __| |/ / __|
| | | | |_) \__ \_ | || | | | (__| <\__ \ | (_> < | _ | (_| | (__| <\__ \
|_| |_| .__/|___( ) |_||_| |_|\___|_|\_\___/ \___/\/ |_| |_|\__,_|\___|_|\_\___/
|_| |/
created by: http://www.ascii-art-generator.org/
for https://www.meetup.com/The-San-Francisco-Django-Meetup-Group/events/234806317/
"""
##############################################################
# What is DRF?
##############################################################
"""
http://www.django-rest-framework.org/
"""
##############################################################
# Read the docs!
##############################################################
"""
http://www.django-rest-framework.org/
Tutorial: http://www.django-rest-framework.org/tutorial/quickstart/
Quotes: http://www.django-rest-framework.org/api-guide/relations/#serializer-relations
"""
##############################################################
# Even Better...read the source code ;)
##############################################################
# rest_framework/serializers ln 63
# class ModelSerializer
##############################################################
# The Danger of Defaults: fields
##############################################################
# Bad
# until 3.5 the default was fields = '__all__'
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = '__all__'
# Better
# now you must explicitly list the `__all__` shortcut or use `fields` or `excludes`
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = ('id', 'account_name', 'users', 'created')
# ...but
# python manage.py shell
>>> serializer = AccountSerializer()
>>> print(repr(serializer))
AccountSerializer():
id = IntegerField(label='ID', read_only=True)
name = CharField(allow_blank=True, max_length=100, required=False)
owner = PrimaryKeyRelatedField(queryset=User.objects.all())
# printing serializers very helpful when: nested, debugging
# Best, explicitly list all fields
# examples: boolean fields, custom fields, readonly, writeonly etc
# If its too tedious to list all fields...
# see rest_framekwork/serializers ln 799
AccountSerializer.serializer_field_mapping[models.NullBooleanField] = \
serializers.BooleanField
##############################################################
# The Danger of Defaults: views
##############################################################
# from rest_framework/views
class APIView(View):
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
settings = api_settings
exclude_from_schema = False
# from rest_framework/generics
class GenericAPIView(views.APIView):
queryset = None
serializer_class = None
lookup_field = 'pk'
lookup_url_kwarg = None
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
# from rest_framework/mixins
class CreateModelMixin(object):
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError):
return {}
# from rest_framework/generics
class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
# YOUR ACTUAL VIEW
class CustomerRegistrationView(CreateAPIView):
def perform_create(self, serializer):
# do crazy STUFF!!!
serializer.save()
##############################################################
# The Danger of Defaults: views
##############################################################
# my single contribution to opensource
# https://github.com/GetBlimp/django-rest-framework-jwt/pull/138/files
import inspect
from django.conf import settings
from django.test import TestCase
def get_module_classes(module):
clsmembers = inspect.getmembers(views, inspect.isclass)
return [clsmember[1] for clsmember in clsmembers]
class TestViews(TestCase):
def test_all_views_are_throttled_as_expected(self):
unthrottled_views = [view for view in get_module_classes(views)
if getattr(view, 'throttle_scope', '') != settings.REST_FRAMEWORK_CUSTOMER_PORTAL_THROTTLE]
self.assertEqual(unthrottled_views, [])
def test_all_views_are_permissioned_as_expected(self):
view_dict = {}
for view in get_module_classes(views):
permissions = [perm.__name__ for perm in view.permission_classes]
view_dict.update({view.__name__: permissions})
expected = {
'CustomerApplicationView': ['IsAuthenticated', 'IsCustomer', 'IsAppOwner'],
'CustomerChangePasswordView': ['IsAuthenticated', 'IsCustomer'],
'CustomerRegistrationView': ['AllowAny'],
}
self.assertEqual(view_dict, expected)
##############################################################
# The Danger of Defaults: views
##############################################################
# if its too permissive write your own...
class PatchModelMixin(object):
def partial_update(self, request, *args, **kwargs):
"""
Partial update for a model instance
used this instead of rest_framework.generics.UpdateModelMixin, when PUTs are not allowed
"""
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
# from rest_framework/mixins
class UpdateModelMixin(object):
"""
Update a model instance.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# refresh the instance from the database.
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
##############################################################
# Tips: Logic on the server side
##############################################################
# URLS
urlpatterns = [
url(r'^registration/$', views.CustomerRegistrationView.as_view(), name='registration'),
]
# VIEW
class CustomerRegistrationView(generics.CreateAPIView):
# perform_create is called by CreateAPIView.create after checking that serializer.is_valid()
def perform_create(self, serializer):
serializer.save() # calls serializer.create()
# send some notifications
# check some things
# log `SUCCESSFUL-REGISTRATION`
# SERIALIZER
class CustomerRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = '__all__'
def create(self, validated_data):
self.customer = self._create_customer(validated_data)
self.application = self._create_application(self.customer)
self.borrower = self._create_borrower(self.application)
self.borrower_incomes = self._create_incomes(self.borrower)
self.borrower_assets = self._create_assets(self.borrower)
self.borrower_expenses = self._create_expenses(self.borrower)
return self.customer
##############################################################
# Tips: overwrite methods predictably
##############################################################
# http://www.django-rest-framework.org/api-guide/generic-views/#methods
# VIEW
class CustomerRegistrationView(generics.CreateAPIView):
# perform_create is called by CreateAPIView.create after checking that serializer.is_valid()
def perform_create(self, serializer):
serializer.save() # calls serializer.create()
# send some notifications
# check some things
# log `SUCCESSFUL-REGISTRATION`
# SERIALIZER
class CustomerRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = '__all__'
def create(self, validated_data):
self.customer = self._create_customer(validated_data)
self.application = self._create_application(self.customer)
self.borrower = self._create_borrower(self.application)
self.borrower_incomes = self._create_incomes(self.borrower)
self.borrower_assets = self._create_assets(self.borrower)
self.borrower_expenses = self._create_expenses(self.borrower)
return self.customer # serializer.save() saves what is returned here to serializer.instance
##############################################################
# Tips: how to pass data between views & serializers
##############################################################
"""
lets say I have session information that needs to be associated with my customer...
I need that data in:
- the view (to send a notification)
- the serializer (to create the relationship in the db)
"""
# VIEW
class CustomerRegistrationView(generics.CreateAPIView):
# perform_create is called by CreateAPIView.create after checking that serializer.is_valid()
def perform_create(self, serializer):
my_object = _get_object_from_session(self.request.session)
self._send_notification(my_object)
serializer.my_object = my_object # required for in serializer.create()
serializer.save() # calls serializer.create()
# SERIALIZER
class CustomerRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = '__all__'
def create(self, validated_data):
assert self.my_object, 'serializer.my_object must be set in view before .save() is called'
self.customer = self._create_customer(validated_data, self.my_object)
# self.application = self._create_application(self.customer)
# self.borrower = self._create_borrower(self.application)
# self.borrower_incomes = self._create_incomes(self.borrower)
# self.borrower_assets = self._create_assets(self.borrower)
# self.borrower_expenses = self._create_expenses(self.borrower)
return self.customer
# OTHER WAYS to pass info from view to serializer
# example 1: pass it to the context in the view
class CustomerRegistrationView(generics.CreateAPIView):
def perform_create(self, serializer):
my_object = _get_object_from_session(self.request.session)
serlializer.context['my_object'] = my_object
serializer.save()
class CustomerRegistrationSerializer(serializers.ModelSerializer):
def create(self, validated):
my_object = self.context['my_object']
self.customer = self._create_customer(validated_data, my_object)
return self.customer
# example 2: overwrite get_serializer_context to pass in my_object
class CustomerRegistrationView(generics.CreateAPIView):
def get_serializer_context(self):
my_object = _get_object_from_session(self.request.session)
return {
'request': self.request,
'format': self.format_kwarg,
'view': self,
'my_object': my_object
}
class CustomerRegistrationSerializer(serializers.ModelSerializer):
def create(self, validated_data):
my_object = self.context['my_object']
self.customer = self._create_customer(validated_data, my_object)
return self.customer
# example 3: reuse a method between view and serializer
class CustomerRegistrationView(generics.CreateAPIView):
def perform_create(self, serializer):
my_object = _get_object_from_session(self.request.session)
class CustomerRegistrationSerializer(serializers.ModelSerializer):
def create(self, validated):
my_object = _get_object_from_session(self.context['request'].session)
self.customer = self._create_customer(validated_data, my_object)
return self.customer
# example 4 : access the information on the view
class CustomerRegistrationView(generics.CreateAPIView):
def perform_create(self, serializer):
self.my_object = _get_object_from_session(self.request.session)
serializer.save() # calls serializer.create()
class CustomerRegistrationSerializer(serializers.ModelSerializer):
def perform_create(self, serlializer):
my_object = self.context['view'].my_object
self.customer = self._create_customer(validated_data, my_object)
return self.customer
##############################################################
# Tips: put things in the view
##############################################################
"""
Django has a great saying "Fat Models, thin views"...but not in this case
don't hide things...
"""
# BAD
"""
Why
- my view is "thin", but only because I've hidden things
- I've overwritten an unexpected method, unless you're familiar with DRF, the person after you
likely has no idea where this method comes from
"""
class CustomerRegistrationView(generics.CreateAPIView):
def get_serializer_context(self):
my_object = _get_object_from_session(self.request.session)
return {
'request': self.request,
'format': self.format_kwarg,
'view': self,
'my_object': my_object
}
# this is likely in another file...
class CustomerRegistrationSerializer(serializers.ModelSerializer):
def create(self, validated_data):
my_object = self.context['my_object']
_send_notification(my_object)
self.customer = self._create_customer(validated_data, my_object)
return self.customer
# BETTER
"""
Why:
- I can immeditately see everything that is happening
- I've overwritten an expected method
"""
class CustomerRegistrationView(generics.CreateAPIView):
# perform_create is called by CreateAPIView.create after checking that serializer.is_valid()
def perform_create(self, serializer):
my_object = _get_object_from_session(self.request.session)
self._send_notification(my_object)
serializer.my_object # required in serializer.create()
serializer.save() # calls serializer.create()
class CustomerRegistrationSerializer(serializers.ModelSerializer):
def create(self, validated_data):
self.customer = self._create_customer(validated_data, self.my_object)
return self.customer
##############################################################
# Tips: 404s not 403s
##############################################################
# Here's a 403
"""
why its a problem:
- have to look in two places
- doesn't scale well...
- override the default permissions can lead to subtle bugs
"""
class IsApplicationOwner(BasePermission):
def has_permission(self, request, view):
apps = Application.objects.filter(id=view.kwargs['id'], customer=request.user.id)
return apps.exists()
class ApplicationView(BaseMixin, PatchModelMixin):
"""
partial_update handles PATCH request to update the Application:
* path: /application/<id>/
"""
permission_classes = (IsAuthenticated, IsCustomer, IsApplicationOwner)
queryset = Application.objects.all()
# Here's a 404
"""
why better:
- look in one place
- not overwriting default permissions
- doesn't leak information about whether or not an object exists
"""
class ApplicationView(BaseMixin, PatchModelMixin):
"""
partial_update handles PATCH request to update the Application:
* path: /application/<id>/
"""
def get_queryset(self):
return Application.objects.filter(
is_active=True,
id=self.kwargs['id'],
application_customer=self.request.user.id,
)
##############################################################
# FINAL PART of the presentation: HACKS
##############################################################
##############################################################
# HACK 1: polymorphic serialization
##############################################################
"""
before we had:
- 2 views PurchaseLoanView, RefinanceLoanView
- 2 endpoints /loans/purchase/<id>
/loans/refinance/<id>
now:
- 1 view, 1 endpoint, simplfies API and client
"""
class LoanView(BaseMixin, PatchModelMixin, mixins.RetrieveModelMixin):
"""
LIST METHODS: None
DETAIL METHODS: cp:mps-detail, loans/<id>
- GET RetrieveModelMixin
- PATCH PatchModelMixin
- PUT not allowed
"""
def get_serializer(self, instance, *args, **kwargs):
"""
handles polymorphic serialization by selecting the serializer class based on the class of
the object returned by .get_object and passed into .get_serializer as 'instance'
note: overwrote .get_serializer instead of .get_serializer_class, which is suggested,
since wanted access to the instance to determine which serializer_class to use.
"""
kwargs['context'] = self.get_serializer_context()
if isinstance(instance, LoanPurchase):
return LoanPurchaseSerializer(instance, *args, **kwargs)
elif isinstance(instance, LoanRefinance):
return LoanRefinanceSerializer(instance, *args, **kwargs)
raise Exception('Unexpected type of object')
##############################################################
# HACK 2: Failure only Throttle
##############################################################
def failure_only_throttle(throttle_class):
"""
a decorator for view functions that will only throttle the request if the view
returns an error
note: a decorator was necessary because throttles that need to determine whether
request has been successful cannot be set in the standard view_cls.throttle_classes,
since the throttles are checked before the view function is called
"""
def decorator(view_func):
@wraps(view_func)
def wrapped(*args, **kwargs):
throttle = throttle_class()
# this will only work for class based views where self is arg[0], request is arg[1]
view, request = args[0], args[1]
# invoke the view
response = view_func(*args, **kwargs)
if is_success(response.status_code):
# this is where the magic happens
# throttle.throttle_success is called by throttle.allow_request and
# by default increments the cache and then returns True
# the below overrides .throttle_success to return True, but NOT increment the cache
# this means the request is not counted against the limit when the request is successful
throttle.throttle_success = lambda: True
allowed = throttle.allow_request(request, view)
if allowed:
return response
else:
# the below raises exceptions.Throttled so no need to return anything
view.throttled(request, throttle.wait())
return wrapped
return decorator
# example usage
class CustomerLoginView(BaseMixin):
@failure_only_throttle(LoginThrottle)
def post(self, request, *args, **kwargs):
# do stuff
##############################################################
# HACK 2.2: Throttle - clear
##############################################################
# see restu_framework/throttles.py ln 146
class BaseThrottle(SimpleRateThrottle):
@classmethod
def clear_cache_for_email(cls, email):
key = cls._create_key_for_email(email)
return cls.cache.delete(key)
class ThrottleOne(BaseThrottle):
scope = 'one'
class ThrottleTwo(BaseThrottle):
scope = 'two'
ThrottleOne.clear_cache_for_email('r@s.com')
ThrottleTwo.clear_cache_for_email('r@s.com')
# SIDE NOTE: django-solo
# very useful for settings you want to pudate without restarting django
# https://github.com/lazybird/django-solo
##############################################################
# HACK 3: Serializers to check state
##############################################################
# One way to check state
def passes_preflight_check(loan):
errors = []
if loan.credit_score < 650:
errors.append('error message')
# check many other conditions
if errors:
raise Exception(errors)
else:
return True
# What is serializers chould check state?
# Usually convert: from data into instance:
serializer = LoanSerializer(data={'credit_score': '650'})
serializer.is_valid()
serializer.save()
# or: from instance into data
serializer = LoanSerializer(loan)
serializer.data
# can't: validate the state of an instance :(
serializer = LoanSerializer(loan)
serializer.is_valid()
# So, create a wrapper:
"""
1. convert from instance into data
2. validate that data
"""
class _PreflightSerializer(serializers.ModelSerializer):
def validate_credit_score(self, score):
if score < 650:
msg = 'credit score of {0} is below 650'.format(score)
raise serializers.ValidationError(msg)
return score
class Meta:
model = Loan
fields = (
'credit_score',
# many many more
)
class PreflightSerializer(_PreflightSerializer):
def __init__(self, loan=None, data=None, **kwargs):
# Step 1: instance into data
data = _PreflightSerializer(loan).data
# Step 2: pass data in for validation
super(PreflightSerializer, self).__init__(loan, data=data)
# Finally: usege is simple :)
PreflightSerializer(loan).is_valid(raise_exception=True)
##############################################################
# Libraries
##############################################################
"""
http://www.django-rest-framework.org/topics/3.5-announcement/
http://getblimp.github.io/django-rest-framework-jwt/
http://chibisov.github.io/drf-extensions/docs/
https://github.com/Axiologue/DjangoRestMultipleModels
"""
##############################################################
# Marshmallow
##############################################################
"""
https://marshmallow.readthedocs.io/en/latest/
Nice Features:
- validate that no other fields are passed in
- multiple errors for a single field
- schema level validation
- dynamically alter what fields a serializer serializes
- libraries for Django, Flask and sql-alchemy compatability
https://github.com/tomchristie/django-rest-marshmallow
https://flask-marshmallow.readthedocs.io/en/latest/
https://marshmallow-sqlalchemy.readthedocs.io/en/latest/
Overview:
https://marshmallow.readthedocs.io/en/latest/why.html
Nice feature Links:
https://marshmallow.readthedocs.org/en/latest/why.html#consistency-meets-flexibility
https://marshmallow.readthedocs.org/en/latest/why.html#context-aware-serialization
https://marshmallow.readthedocs.org/en/latest/nesting.html#two-way-nesting
https://marshmallow.readthedocs.org/en/latest/extending.html#schema-level-validation
https://marshmallow.readthedocs.org/en/latest/extending.html#validating-original-input-data
https://marshmallow.readthedocs.org/en/latest/extending.html#pre-post-processor-invocation-order
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment