个人对drf-extentions 的英文文档的部分整理翻译与保存(个人用)
Posted daysn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了个人对drf-extentions 的英文文档的部分整理翻译与保存(个人用)相关的知识,希望对你有一定的参考价值。
Caching
To cache something is to save the result of an expensive calculation so that you don‘t have to perform the calculation next time. Here‘s some pseudocode explaining how this would work for a dynamically generated api response:
given a URL, try finding that API response in the cache if the response is in the cache: return the cached response else: generate the response save the generated response in the cache (for next time) return the generated response
缓存一些东西是为了保存一个昂贵计算的结果,这样下次就不必执行计算了。下面是一些伪代码,解释了这将如何为动态生成的api响应工作:
给定一个URL,
如果响应在缓存中,尝试在缓存中找到该API响应返回缓存的响应:否则生成响应并将生成的响应保存到缓存中(下次使用)然后返回
Cache response
DRF-extensions allows you to cache api responses with simple @cache_response
decorator. There are two requirements for decorated method:
- It should be method of class which is inherited from
rest_framework.views.APIView
- It should return
rest_framework.response.Response
instance
翻译:DRF-extensions允许您使用简单的@cache_responsedecorator缓存api响应。装饰方法有两个要求:
它应该是类的方法,继承自rest_frame .views. apiview
它应该返回rest_frame .response。响应实例
Usage example:
from rest_framework.response import Response from rest_framework import views from rest_framework_extensions.cache.decorators import ( cache_response ) from myapp.models import City class CityView(views.APIView): @cache_response() def get(self, request, *args, **kwargs): cities = City.objects.all().values_list(‘name‘, flat=True) return Response(cities)
If you request view first time you‘ll get it from processed SQL query. (~60ms response time):
# Request GET /cities/ HTTP/1.1 Accept: application/json # Response HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 [‘Moscow‘, ‘London‘, ‘Paris‘]
Second request will hit the cache. No sql evaluation, no database query. (~30 ms response time):
# Request GET /cities/ HTTP/1.1 Accept: application/json # Response HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 [‘Moscow‘, ‘London‘, ‘Paris‘]
Reduction in response time depends on calculation complexity inside your API method. Sometimes it reduces from 1 second to 10ms, sometimes you win just 10ms.
Timeout
You can specify cache timeout in seconds, providing first argument:
class CityView(views.APIView): @cache_response(60 * 15) def get(self, request, *args, **kwargs): ...
In the above example, the result of the get()
view will be cached for 15 minutes.
If you don‘t specify cache_timeout
argument then value from REST_FRAMEWORK_EXTENSIONS
settings will be used. By default it‘s None
, which means "cache forever". You can change this default in settings:
REST_FRAMEWORK_EXTENSIONS = { ‘DEFAULT_CACHE_RESPONSE_TIMEOUT‘: 60 * 15 }
Usage of the specific cache
New in DRF-extensions 0.2.3
@cache_response
can also take an optional keyword argument, cache
, which directs the decorator to use a specific cache (from your CACHES setting) when caching results. By default, the default
cache will be used, but you can specify any cache you want:
class CityView(views.APIView): @cache_response(60 * 15, cache=‘special_cache‘) def get(self, request, *args, **kwargs): ...
You can specify what cache to use by default in settings:
REST_FRAMEWORK_EXTENSIONS = { ‘DEFAULT_USE_CACHE‘: ‘special_cache‘ }
Cache key
By default every cached data from @cache_response
decorator stored by key, which calculated with DefaultKeyConstructor.
You can change cache key by providing key_func
argument, which must be callable:
def calculate_cache_key(view_instance, view_method, request, args, kwargs): return ‘.‘.join([ len(args), len(kwargs) ]) class CityView(views.APIView): @cache_response(60 * 15, key_func=calculate_cache_key) def get(self, request, *args, **kwargs): ...
You can implement view method and use it for cache key calculation by specifying key_func
argument as string:
class CityView(views.APIView): @cache_response(60 * 15, key_func=‘calculate_cache_key‘) def get(self, request, *args, **kwargs): ... def calculate_cache_key(self, view_instance, view_method, request, args, kwargs): return ‘.‘.join([ len(args), len(kwargs) ])
Key calculation function will be called with next parameters:
- view_instance - view instance of decorated method
- view_method - decorated method
- request - decorated method request
- args - decorated method positional arguments
- kwargs - decorated method keyword arguments
Default key function
If @cache_response
decorator used without key argument then default key function will be used. You can change this function in settings:
REST_FRAMEWORK_EXTENSIONS = { ‘DEFAULT_CACHE_KEY_FUNC‘: ‘rest_framework_extensions.utils.default_cache_key_func‘ }
default_cache_key_func
uses DefaultKeyConstructor as a base for key calculation.
Caching errors
New in DRF-extensions 0.2.7
By default every response is cached, even failed. For example:
class CityView(views.APIView): @cache_response() def get(self, request, *args, **kwargs): raise Exception("500 error comes from here")
First request to CityView.get
will fail with 500
status code error and next requests to this endpoint will return 500
error from cache.
You can change this behaviour by turning off caching error responses:
class CityView(views.APIView): @cache_response(cache_errors=False) def get(self, request, *args, **kwargs): raise Exception("500 error comes from here")
You can change default behaviour by changing DEFAULT_CACHE_ERRORS
setting:
REST_FRAMEWORK_EXTENSIONS = { ‘DEFAULT_CACHE_ERRORS‘: False }
CacheResponseMixin
It is common to cache standard viewset retrieve
and list
methods. That is why CacheResponseMixin
exists. Just mix it into viewset implementation and those methods will use functions, defined in REST_FRAMEWORK_EXTENSIONS
settings:
- "DEFAULT_OBJECT_CACHE_KEY_FUNC" for
retrieve
method - "DEFAULT_LIST_CACHE_KEY_FUNC" for
list
method
By default those functions are using DefaultKeyConstructor and extends it:
- With
RetrieveSqlQueryKeyBit
for "DEFAULT_OBJECT_CACHE_KEY_FUNC" - With
ListSqlQueryKeyBit
andPaginationKeyBit
for "DEFAULT_LIST_CACHE_KEY_FUNC"
You can change those settings for custom cache key generation:
REST_FRAMEWORK_EXTENSIONS = { ‘DEFAULT_OBJECT_CACHE_KEY_FUNC‘: ‘rest_framework_extensions.utils.default_object_cache_key_func‘, ‘DEFAULT_LIST_CACHE_KEY_FUNC‘: ‘rest_framework_extensions.utils.default_list_cache_key_func‘, }
Mixin example usage:
from myapps.serializers import UserSerializer from rest_framework_extensions.cache.mixins import CacheResponseMixin class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet): serializer_class = UserSerializer
You can change cache key function by providing object_cache_key_func
orlist_cache_key_func
methods in view class:
class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet): serializer_class = UserSerializer def object_cache_key_func(self, **kwargs): return ‘some key for object‘ def list_cache_key_func(self, **kwargs): return ‘some key for list‘
Ofcourse you can use custom key constructor:
from yourapp.key_constructors import ( CustomObjectKeyConstructor, CustomListKeyConstructor, ) class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet): serializer_class = UserSerializer object_cache_key_func = CustomObjectKeyConstructor() list_cache_key_func = CustomListKeyConstructor()
If you want to cache only retrieve
method then you could use rest_framework_extensions.cache.mixins.RetrieveCacheResponseMixin
.
If you want to cache only list
method then you could use rest_framework_extensions.cache.mixins.ListCacheResponseMixin
.
Key constructor
As you could see from previous section cache key calculation might seem fairly simple operation. But let‘s see next example. We make ordinary HTTP request to cities resource:
# Request GET /cities/ HTTP/1.1 Accept: application/json # Response HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 [‘Moscow‘, ‘London‘, ‘Paris‘]
By the moment all goes fine - response returned and cached. Let‘s make the same request requiring XML response:
# Request GET /cities/ HTTP/1.1 Accept: application/xml # Response HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 [‘Moscow‘, ‘London‘, ‘Paris‘]
What is that? Oh, we forgot about format negotiations. We can add format to key bits:
def calculate_cache_key(view_instance, view_method, request, args, kwargs): return ‘.‘.join([ len(args), len(kwargs), request.accepted_renderer.format # here it is ]) # Request GET /cities/ HTTP/1.1 Accept: application/xml # Response HTTP/1.1 200 OK Content-Type: application/xml; charset=UTF-8 <?xml version="1.0" encoding="utf-8"?> <root> <list-item>Moscow</list-item> <list-item>London</list-item> <list-item>Paris</list-item> </root>
That‘s cool now - we have different responses for different formats with different cache keys. But there are many cases, where key should be different for different requests:
- Response format (json, xml);
- User (exact authorized user or anonymous);
- Different request meta data (request.META[‘REMOTE_ADDR‘]);
- Language (ru, en);
- Headers;
- Query params. For example,
jsonp
resources needcallback
param, which rendered in response; - Pagination. We should show different data for different pages;
- Etc...
Of course we can use custom calculate_cache_key
methods and reuse them for different API methods, but we can‘t reuse just parts of them. For example, one method depends on user id and language, but another only on user id. How to be more DRYish? Let‘s see some magic:
from rest_framework_extensions.key_constructor.constructors import ( KeyConstructor ) from rest_framework_extensions.key_constructor import bits from your_app.utils import get_city_by_ip class CityGetKeyConstructor(KeyConstructor): unique_method_id = bits.UniqueMethodIdKeyBit() format = bits.FormatKeyBit() language = bits.LanguageKeyBit() class CityHeadKeyConstructor(CityGetKeyConstructor): user = bits.UserKeyBit() request_meta = bits.RequestMetaKeyBit(params=[‘REMOTE_ADDR‘]) class CityView(views.APIView): @cache_response(key_func=CityGetKeyConstructor()) def get(self, request, *args, **kwargs): cities = City.objects.all().values_list(‘name‘, flat=True) return Response(cities) @cache_response(key_func=CityHeadKeyConstructor()) def head(self, request, *args, **kwargs): city = ‘‘ user = self.request.user if user.is_authenticated() and user.city: city = Response(user.city.name) if not city: city = get_city_by_ip(request.META[‘REMOTE_ADDR‘]) return Response(city)
Firstly, let‘s revise CityView.get
method cache key calculation. It constructs from 3 bits:
- unique_method_id - remember that default key calculation? Here it is. Just one of the cache key bits.
head
method has different set of bits and they can‘t collide withget
method bits. But there could be another view class with the same bits. - format - key would be different for different formats.
- language - key would be different for different languages.
The second method head
has the same unique_method_id
, format
and language
bits, buts extends with 2 more:
- user - key would be different for different users. As you can see in response calculation we use
request.user
instance. For different users we need different responses. - request_meta - key would be different for different ip addresses. As you can see in response calculation we are falling back to getting city from ip address if couldn‘t get it from authorized user model.
All default key bits are listed in this section.
Default key constructor
DefaultKeyConstructor
is located in rest_framework_extensions.key_constructor.constructors
module and constructs a key from unique method id, request format and request language. It has next implementation:
class DefaultKeyConstructor(KeyConstructor): unique_method_id = bits.UniqueMethodIdKeyBit() format = bits.FormatKeyBit() language = bits.LanguageKeyBit()
How key constructor works
Key constructor class works in the same manner as the standard django forms and key bits used like form fields. Lets go through key construction steps for DefaultKeyConstructor.
Firstly, constructor starts iteration over every key bit:
- unique_method_id
- format
- language
Then constructor gets data from every key bit calling method get_data
:
- unique_method_id -
u‘your_app.views.SometView.get‘
- format -
u‘json‘
- language -
u‘en‘
Every key bit get_data
method is called with next arguments:
- view_instance - view instance of decorated method
- view_method - decorated method
- request - decorated method request
- args - decorated method positional arguments
- kwargs - decorated method keyword arguments
After this it combines every key bit data to one dict, which keys are a key bits names in constructor, and values are returned data:
{ ‘unique_method_id‘: u‘your_app.views.SometView.get‘, ‘format‘: u‘json‘, ‘language‘: u‘en‘ }
Then constructor dumps resulting dict to json:
‘{"unique_method_id": "your_app.views.SometView.get", "language": "en", "format": "json"}‘
And finally compresses json with md5 and returns hash value:
‘b04f8f03c89df824e0ecd25230a90f0e0ebe184cf8c0114342e9471dd2275baa‘
Custom key bit
We are going to create a simple key bit which could be used in real applications with next properties:
- High read rate
- Low write rate
The task is - cache every read request and invalidate all cache data after write to any model, which used in API. This approach let us don‘t think about granular cache invalidation - just flush it after any model instance change/creation/deletion.
Lets create models:
# models.py from django.db import models class Group(models.Model): title = models.CharField() class Profile(models.Model): name = models.CharField() group = models.ForeignKey(Group)
Define serializers:
# serializers.py from yourapp.models import Group, Profile from rest_framework import serializers class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group class ProfileSerializer(serializers.ModelSerializer): group = GroupSerializer() class Meta: model = Profile
Create views:
# views.py from yourapp.serializers import GroupSerializer, ProfileSerializer from yourapp.models import Group, Profile class GroupViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = GroupSerializer queryset = Group.objects.all() class ProfileViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = ProfileSerializer queryset = Profile.objects.all()
And finally register views in router:
# urls.py from yourapp.views import GroupViewSet,ProfileViewSet router = DefaultRouter() router.register(r‘groups‘, GroupViewSet) router.register(r‘profiles‘, ProfileViewSet) urlpatterns = router.urls
At the moment we have API, but it‘s not cached. Lets cache it and create our custom key bit:
# views.py import datetime from django.core.cache import cache from django.utils.encoding import force_text from yourapp.serializers import GroupSerializer, ProfileSerializer from rest_framework_extensions.cache.decorators import cache_response from rest_framework_extensions.key_constructor.constructors import ( DefaultKeyConstructor ) from rest_framework_extensions.key_constructor.bits import ( KeyBitBase, RetrieveSqlQueryKeyBit, ListSqlQueryKeyBit, PaginationKeyBit ) class UpdatedAtKeyBit(KeyBitBase): def get_data(self, **kwargs): key = ‘api_updated_at_timestamp‘ value = cache.get(key, None) if not value: value = datetime.datetime.utcnow() cache.set(key, value=value) return force_text(value) class CustomObjectKeyConstructor(DefaultKeyConstructor): retrieve_sql = RetrieveSqlQueryKeyBit() updated_at = UpdatedAtKeyBit() class CustomListKeyConstructor(DefaultKeyConstructor): list_sql = ListSqlQueryKeyBit() pagination = PaginationKeyBit() updated_at = UpdatedAtKeyBit() class GroupViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = GroupSerializer @cache_response(key_func=CustomObjectKeyConstructor()) def retrieve(self, *args, **kwargs): return super(GroupViewSet, self).retrieve(*args, **kwargs) @cache_response(key_func=CustomListKeyConstructor()) def list(self, *args, **kwargs): return super(GroupViewSet, self).list(*args, **kwargs) class ProfileViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = ProfileSerializer @cache_response(key_func=CustomObjectKeyConstructor()) def retrieve(self, *args, **kwargs): return super(ProfileViewSet, self).retrieve(*args, **kwargs) @cache_response(key_func=CustomListKeyConstructor()) def list(self, *args, **kwargs): return super(ProfileViewSet, self).list(*args, **kwargs)
As you can see UpdatedAtKeyBit
just adds to key information when API models has been update last time. If there is no information about it then new datetime will be used for key bit data.
Lets write cache invalidation. We just connect models to standard signals and change value in cache by key api_updated_at_timestamp
:
# models.py import datetime from django.db import models from django.db.models.signals import post_save, post_delete def change_api_updated_at(sender=None, instance=None, *args, **kwargs): cache.set(‘api_updated_at_timestamp‘, datetime.datetime.utcnow()) class Group(models.Model): title = models.CharField() class Profile(models.Model): name = models.CharField() group = models.ForeignKey(Group) for model in [Group, Profile]: post_save.connect(receiver=change_api_updated_at, sender=model) post_delete.connect(receiver=change_api_updated_at, sender=model)
And that‘s it. When any model changes then value in cache by key api_updated_at_timestamp
will be changed too. After this every key constructor, that used UpdatedAtKeyBit
, will construct new keys and @cache_response
decorator will cache data in new places.
Key constructor params
New in DRF-extensions 0.2.3
You can change params
attribute for specific key bit by providing params
dict for key constructor initialization function. For example, here is custom key constructor, which inherits from DefaultKeyConstructor and adds geoip key bit:
class CityKeyConstructor(DefaultKeyConstructor): geoip = bits.RequestMetaKeyBit(params=[‘GEOIP_CITY‘])
If you wanted to use GEOIP_COUNTRY
, you could create new key constructor:
class CountryKeyConstructor(DefaultKeyConstructor): geoip = bits.RequestMetaKeyBit(params=[‘GEOIP_COUNTRY‘])
But there is another way. You can send params
in key constructor initialization method. This is the dict attribute, where keys are bit names and values are bit params
attribute value (look at CountryView
):
class CityKeyConstructor(DefaultKeyConstructor): geoip = bits.RequestMetaKeyBit(params=[‘GEOIP_COUNTRY‘]) class CityView(views.APIView): @cache_response(key_func=CityKeyConstructor()) def get(self, request, *args, **kwargs): ... class CountryView(views.APIView): @cache_response(key_func=CityKeyConstructor( params={‘geoip‘: [‘GEOIP_COUNTRY‘]} )) def get(self, request, *args, **kwargs): ...
If there is no item provided for key bit then default key bit params
value will be used.
Constructor‘s bits list
You can dynamically change key constructor‘s bits list in initialization method by altering bits
attribute:
class CustomKeyConstructor(DefaultKeyConstructor): def __init__(self, *args, **kwargs): super(CustomKeyConstructor, self).__init__(*args, **kwargs) self.bits[‘geoip‘] = bits.RequestMetaKeyBit( params=[‘GEOIP_CITY‘] )
Default key bits
Out of the box DRF-extensions has some basic key bits. They are all located in rest_framework_extensions.key_constructor.bits
module.
FormatKeyBit
Retrieves format info from request. Usage example:
class MyKeyConstructor(KeyConstructor): format = FormatKeyBit()
LanguageKeyBit
Retrieves active language for request. Usage example:
class MyKeyConstructor(KeyConstructor): user = LanguageKeyBit()
UserKeyBit
Retrieves user id from request. If it is anonymous then returnes "anonymous" string. Usage example:
class MyKeyConstructor(KeyConstructor): user = UserKeyBit()
RequestMetaKeyBit
Retrieves data from request.META dict. Usage example:
class MyKeyConstructor(KeyConstructor): ip_address_and_user_agent = bits.RequestMetaKeyBit( [‘REMOTE_ADDR‘, ‘HTTP_USER_AGENT‘] )
You can use *
for retrieving all meta data to key bit:
New in DRF-extensions 0.2.7
class MyKeyConstructor(KeyConstructor): all_request_meta = bits.RequestMetaKeyBit(‘*‘)
HeadersKeyBit
Same as RequestMetaKeyBit
retrieves data from request.META dict. The difference is that HeadersKeyBit
allows to use normal header names:
class MyKeyConstructor(KeyConstructor): user_agent_and_geobase_id = bits.HeadersKeyBit( [‘user-agent‘, ‘x-geobase-id‘] ) # will process request.META[‘HTTP_USER_AGENT‘] and # request.META[‘HTTP_X_GEOBASE_ID‘]
You can use *
for retrieving all headers to key bit:
New in DRF-extensions 0.2.7
class MyKeyConstructor(KeyConstructor): all_headers = bits.HeadersKeyBit(‘*‘)
ArgsKeyBit
New in DRF-extensions 0.2.7
Retrieves data from the view‘s positional arguments. A list of position indices can be passed to indicate which arguments to use. For retrieving all arguments you can use *
which is also the default value:
class MyKeyConstructor(KeyConstructor): args = bits.ArgsKeyBit() # will use all positional arguments class MyKeyConstructor(KeyConstructor): args = bits.ArgsKeyBit(‘*‘) # same as above class MyKeyConstructor(KeyConstructor): args = bits.ArgsKeyBit([0, 2])
KwargsKeyBit
New in DRF-extensions 0.2.7
Retrieves data from the views‘s keyword arguments. A list of keyword argument names can be passed to indicate which kwargs to use. For retrieving all kwargs you can use *
which is also the default value:
class MyKeyConstructor(KeyConstructor): kwargs = bits.KwargsKeyBit() # will use all keyword arguments class MyKeyConstructor(KeyConstructor): kwargs = bits.KwargsKeyBit(‘*‘) # same as above class MyKeyConstructor(KeyConstructor): kwargs = bits.KwargsKeyBit([‘user_id‘, ‘city‘])
QueryParamsKeyBit
Retrieves data from request.GET dict. Usage example:
class MyKeyConstructor(KeyConstructor): part_and_callback = bits.QueryParamsKeyBit( [‘part‘, ‘callback‘] )
You can use *
for retrieving all query params to key bit which is also the default value:
New in DRF-extensions 0.2.7
class MyKeyConstructor(KeyConstructor): all_query_params = bits.QueryParamsKeyBit(‘*‘) # all qs parameters class MyKeyConstructor(KeyConstructor): all_query_params = bits.QueryParamsKeyBit() # same as above
PaginationKeyBit
Inherits from QueryParamsKeyBit
and returns data from used pagination params.
class MyKeyConstructor(KeyConstructor): pagination = bits.PaginationKeyBit()
ListSqlQueryKeyBit
Retrieves sql query for view.filter_queryset(view.get_queryset())
filtering.
class MyKeyConstructor(KeyConstructor): list_sql_query = bits.ListSqlQueryKeyBit()
RetrieveSqlQueryKeyBit
Retrieves sql query for retrieving exact object.
class MyKeyConstructor(KeyConstructor): retrieve_sql_query = bits.RetrieveSqlQueryKeyBit()
UniqueViewIdKeyBit
Combines data about view module and view class name.
class MyKeyConstructor(KeyConstructor): unique_view_id = bits.UniqueViewIdKeyBit()
UniqueMethodIdKeyBit
Combines data about view module, view class name and view method name.
class MyKeyConstructor(KeyConstructor): unique_view_id = bits.UniqueMethodIdKeyBit()
Conditional requests
This documentation section uses information from RESTful Web Services Cookbook 10-th chapter.
Conditional HTTP request allows API clients to accomplish 2 goals:
- Conditional HTTP GET saves client and server time and bandwidth.
- For unsafe requests such as PUT, POST, and DELETE, conditional requests provide concurrency control.
HTTP Etag
An ETag or entity tag, is part of HTTP, the protocol for the World Wide Web. It is one of several mechanisms that HTTP provides for web cache validation, and which allows a client to make conditional requests. - Wikipedia
For etag calculation and conditional request processing you should use rest_framework_extensions.etag.decorators.etag
decorator. It‘s similar to native django decorator.
from rest_framework_extensions.etag.decorators import etag class CityView(views.APIView): @etag() def get(self, request, *args, **kwargs): cities = City.objects.all().values_list(‘name‘, flat=True) return Response(cities)
By default @etag
would calculate header value with the same algorithm as cache keydefault calculation performs.
# Request GET /cities/ HTTP/1.1 Accept: application/json # Response HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 ETag: "e7b50490dc546d116635a14cfa58110306dd6c5434146b6740ec08bf0a78f9a2" [‘Moscow‘, ‘London‘, ‘Paris‘]
You can define custom function for Etag value calculation with etag_func
argument:
from rest_framework_extensions.etag.decorators import etag def calculate_etag(view_instance, view_method, request, args, kwargs): return ‘.‘.join([ len(args), len(kwargs) ]) class CityView(views.APIView): @etag(etag_func=calculate_etag) def get(self, request, *args, **kwargs): cities = City.objects.all().values_list(‘name‘, flat=True) return Response(cities)
You can implement view method and use it for Etag calculation by specifying etag_func
argument as string:
from rest_framework_extensions.etag.decorators import etag class CityView(views.APIView): @etag(etag_func=‘calculate_etag_from_method‘) def get(self, request, *args, **kwargs): cities = City.objects.all().values_list(‘name‘, flat=True) return Response(cities) def calculate_etag_from_method(self, view_instance, view_method, request, args, kwargs): return ‘.‘.join([ len(args), len(kwargs) ])
Etag calculation function will be called with next parameters:
- view_instance - view instance of decorated method
- view_method - decorated method
- request - decorated method request
- args - decorated method positional arguments
- kwargs - decorated method keyword arguments
Default etag function
If @etag
decorator used without etag_func
argument then default etag function will be used. You can change this function in settings:
REST_FRAMEWORK_EXTENSIONS = { ‘DEFAULT_ETAG_FUNC‘: ‘rest_framework_extensions.utils.default_etag_func‘ }
default_etag_func
uses DefaultKeyConstructor as a base for etag calculation.
Usage with caching
As you can see @etag
and @cache_response
decorators has similar key calculation approaches. They both can take key from simple callable function. And more then this - in many cases they share the same calculation logic. In the next example we use both decorators, which share one calculation function:
from rest_framework_extensions.etag.decorators import etag from rest_framework_extensions.cache.decorators import cache_response from rest_framework_extensions.key_constructor import bits from rest_framework_extensions.key_constructor.constructors import ( KeyConstructor ) class CityGetKeyConstructor(KeyConstructor): format = bits.FormatKeyBit() language = bits.LanguageKeyBit() class CityView(views.APIView): key_constructor_func = CityGetKeyConstructor() @etag(key_constructor_func) @cache_response(key_func=key_constructor_func) def get(self, request, *args, **kwargs): cities = City.objects.all().values_list(‘name‘, flat=True) return Response(cities)
Note the decorators order. First goes @etag
and after goes @cache_response
. We want firstly perform conditional processing and after it response processing.
There is one more point for it. If conditional processing didn‘t fail then key_constructor_func
would be called again in @cache_response
. But in most cases first calculation is enough. To accomplish this goal you could use KeyConstructor
initial argument memoize_for_request
:
>>> key_constructor_func = CityGetKeyConstructor(memoize_for_request=True) >>> request1, request1 = ‘request1‘, ‘request2‘ >>> print key_constructor_func(request=request1) # full calculation request1-key >>> print key_constructor_func(request=request1) # data from cache request1-key >>> print key_constructor_func(request=request2) # full calculation request2-key >>> print key_constructor_func(request=request2) # data from cache request2-key
By default memoize_for_request
is False
, but you can change it in settings:
REST_FRAMEWORK_EXTENSIONS = { ‘DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST‘: True }
It‘s important to note that this memoization is thread safe.
Saving time and bandwith
When a server returns ETag
header, you should store it along with the representation data on the client. When making GET and HEAD requests for the same resource in the future, include the If-None-Match
header to make these requests "conditional".
For example, retrieve all cities:
# Request GET /cities/ HTTP/1.1 Accept: application/json # Response HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 ETag: "some_etag_value" [‘Moscow‘, ‘London‘, ‘Paris‘]
If you make same request with If-None-Match
and there is the cached value for this request, then server will respond with 304
status code without body data.
# Request GET /cities/ HTTP/1.1 Accept: application/json If-None-Match: some_etag_value # Response HTTP/1.1 304 NOT MODIFIED Content-Type: application/json; charset=UTF-8 Etag: "some_etag_value"
After this response you can use existing cities data on the client.
Concurrency control
Concurrency control ensures the correct processing of data under concurrent operations by clients. There are two ways to implement concurrency control:
- Pessimistic concurrency control. In this model, the client gets a lock, obtains the current state of the resource, makes modifications, and then releases the lock. During this process, the server prevents other clients from acquiring a lock on the same resource. Relational databases operate in this manner.
- Optimistic concurrency control. In this model, the client first gets a token. Instead of obtaining a lock, the client attempts a write operation with the token included in the request. The operation succeeds if the token is still valid and fails otherwise.
HTTP, being a stateless application control, is designed for optimistic concurrency control.
PUT | +-------------+ | Etag | | supplied? | +-------------+ | | Yes No | | +--------------------+ +-----------------------+ | Do preconditions | | Does the | | match? | | resource exist? | +--------------------+ +-----------------------+ | | | | Yes No Yes No | | | | +--------------+ +--------------------+ +-------------+ | | Update the | | 412 Precondition | | 403 | | | resource | | failed | | Forbidden | | +--------------+ +--------------------+ +-------------+ | | +-----------------------+ | Can clients | | create resources | +-----------------------+ | | Yes No | | +-----------+ +-------------+ | 201 | | 404 | | Created | | Not Found | +-----------+ +-------------+
Delete:
DELETE | +-------------+ | Etag | | supplied? | +-------------+ | | Yes No | | +--------------------+ +-------------+ | Do preconditions | | 403 | | match? | | Forbidden | +--------------------+ +-------------+ | | Yes No | | +--------------+ +--------------------+ | Delete the | | 412 Precondition | | resource | | failed | +--------------+ +--------------------+
Here is example of implementation for all CRUD methods (except create, because it doesn‘t need concurrency control) wrapped with etag
decorator:
from rest_framework.viewsets import ModelViewSet from rest_framework_extensions.key_constructor import bits from rest_framework_extensions.key_constructor.constructors import ( KeyConstructor ) from your_app.models import City from your_app.key_bits import UpdatedAtKeyBit class CityListKeyConstructor(KeyConstructor): format = bits.FormatKeyBit() language = bits.LanguageKeyBit() pagination = bits.PaginationKeyBit() list_sql_query = bits.ListSqlQueryKeyBit() unique_view_id = bits.UniqueViewIdKeyBit() class CityDetailKeyConstructor(KeyConstructor): format = bits.FormatKeyBit() language = bits.LanguageKeyBit() retrieve_sql_query = bits.RetrieveSqlQueryKeyBit() unique_view_id = bits.UniqueViewIdKeyBit() updated_at = UpdatedAtKeyBit() class CityViewSet(ModelViewSet): list_key_func = CityListKeyConstructor( memoize_for_request=True ) obj_key_func = CityDetailKeyConstructor( memoize_for_request=True ) @etag(list_key_func) @cache_response(key_func=list_key_func) def list(self, request, *args, **kwargs): return super(CityViewSet, self).list(request, *args, **kwargs) @etag(obj_key_func) @cache_response(key_func=obj_key_func) def retrieve(self, request, *args, **kwargs): return super(CityViewSet, self).retrieve(request, *args, **kwargs) @etag(obj_key_func) def update(self, request, *args, **kwargs): return super(CityViewSet, self).update(request, *args, **kwargs) @etag(obj_key_func) def destroy(self, request, *args, **kwargs): return super(CityViewSet, self).destroy(request, *args, **kwargs)
Etag for unsafe methods
From previous section you could see that unsafe methods, such update
(PUT, PATCH) or destroy
(DELETE), have the same @etag
decorator wrapping manner as the safe methods.
But every unsafe method has one distinction from safe method - it changes the data which could be used for Etag calculation. In our case it is UpdatedAtKeyBit
. It means that we should calculate Etag:
- Before building response - for
If-Match
andIf-None-Match
conditions validation - After building response (if necessary) - for clients
@etag
decorator has special attribute rebuild_after_method_evaluation
, which by default is False
.
If you specify rebuild_after_method_evaluation
as True
then Etag will be rebuilt after method evaluation:
class CityViewSet(ModelViewSet): ... @etag(obj_key_func, rebuild_after_method_evaluation=True) def update(self, request, *args, **kwargs): return super(CityViewSet, self).update(request, *args, **kwargs) @etag(obj_key_func) def destroy(self, request, *args, **kwargs): return super(CityViewSet, self).destroy(request, *args, **kwargs) # Request PUT /cities/1/ HTTP/1.1 Accept: application/json {"name": "London"} # Response HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 ETag: "4e63ef056f47270272b96523f51ad938b5ea141024b767880eac047d10a0b339" { id: 1, name: "London" }
As you can see we didn‘t specify rebuild_after_method_evaluation
for destroy
method. That is because there is no sense to use returned Etag value on clients if object deletion already performed.
With rebuild_after_method_evaluation
parameter Etag calculation for PUT
/PATCH
method would look like:
+--------------+ | Request | +--------------+ | +--------------------------+ | Calculate Etag | | for condition matching | +--------------------------+ | +--------------------+ | Do preconditions | | match? | +--------------------+ | | Yes No | | +--------------+ +--------------------+ | Update the | | 412 Precondition | | resource | | failed | +--------------+ +--------------------+ | +--------------------+ | Calculate Etag | | again and add it | | to response | +--------------------+ | +------------+ | Return | | response | +------------+
If-None-Match
example for DELETE
method:
# Request DELETE /cities/1/ HTTP/1.1 Accept: application/json If-None-Match: some_etag_value # Response HTTP/1.1 304 NOT MODIFIED Content-Type: application/json; charset=UTF-8 Etag: "some_etag_value"
If-Match
example for DELETE
method:
# Request DELETE /cities/1/ HTTP/1.1 Accept: application/json If-Match: another_etag_value # Response HTTP/1.1 412 PRECONDITION FAILED Content-Type: application/json; charset=UTF-8 Etag: "some_etag_value"
以上是关于个人对drf-extentions 的英文文档的部分整理翻译与保存(个人用)的主要内容,如果未能解决你的问题,请参考以下文章