Created
October 27, 2016 04:16
-
-
Save resalisbury/f409f2b1d51028c186e9b6eb9844917f to your computer and use it in GitHub Desktop.
Presentation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
____ ____ _____ | |
| _ \| _ \| ___| | |
| | | | |_) | | | |
| |_| | _ <| _| | |
|____/|_| \_\_| | |
_____ _ _____ _ _ ___ _ _ _ | |
|_ _(_)_ __ ___ |_ _| __(_) ___| | _____ ( _ ) | | | | __ _ ___| | _____ | |
| | | | '_ \/ __| | || '__| |/ __| |/ / __| / _ \/\ | |_| |/ _` |/ __| |/ / __| | |
| | | | |_) \__ \_ | || | | | (__| <\__ \ | (_> < | _ | (_| | (__| <\__ \ | |
|_| |_| .__/|___( ) |_||_| |_|\___|_|\_\___/ \___/\/ |_| |_|\__,_|\___|_|\_\___/ | |
|_| |/ | |
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