Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing RESTful Web Services With Flask - PyCon...

Writing RESTful Web Services With Flask - PyCon 2014

(Presented at PyCon 2014)
Watch here: https://www.youtube.com/watch?v=px_vg9Far1Y

Flask is a web framework for Python based on Werkzeug, Jinja 2 and good intentions. It is considered a micro-framework, but don't get the "micro" part fool you; Flask can do everything the "others" can do, many times in a simpler, leaner way. This session will introduce you to Flask as an engine to build RESTful web services.

Miguel Grinberg

April 12, 2014
Tweet

More Decks by Miguel Grinberg

Other Decks in Programming

Transcript

  1. Who Am I? Who Am I? My blog http://blog.miguelgrinberg.com is

    powered by Flask. I wrote the Flask Mega-Tutorial 18-part series. I wrote several articles on API development with Flask. My most popular Flask extensions: I wrote the book "Flask Web Development" for O'Reilly, in bookstores in May 2014. · · · · Flask-SocketIO (WebSocket communication) Flask-Migrate (Database migrations with Alembic) Flask-HTTPAuth (RESTful authentication) Flask-PageDown (Live Markdown editor) Flask-Moment (Rendering of dates and times) - - - - - · 2/38
  2. Source Code Source Code Github project: https://github.com/miguelgrinberg/api-pycon2014. Runs on Python

    2.7 and 3.3+. Detailed installation and usage instructions in the project's README file. · · · 3/38
  3. Flask 101 Flask 101 f r o m f l

    a s k i m p o r t F l a s k a p p = F l a s k ( _ _ n a m e _ _ ) @ a p p . r o u t e ( " / " ) d e f h e l l o ( ) : r e t u r n " H e l l o W o r l d ! " i f _ _ n a m e _ _ = = " _ _ m a i n _ _ " : a p p . r u n ( ) 4/38
  4. Read Resource Collections API Examples •········· f r o m

    f l a s k i m p o r t j s o n i f y @ a p i . r o u t e ( ' / s t u d e n t s / ' , m e t h o d s = [ ' G E T ' ] ) d e f g e t _ s t u d e n t s ( ) : r e t u r n j s o n i f y ( { ' u r l s ' : [ s . g e t _ u r l ( ) f o r s i n S t u d e n t . q u e r y . a l l ( ) ] } ) G E T requests are used to read collection of resources. The response includes the URLs for all the resources in the collection. The g e t _ u r l ( ) helper method in the model class returns the URL for each resource. Flask's j s o n i f y ( ) helper function renders JSON from a Python dictionary. A successful response has status code 2 0 0 ( O K ) , which is the default. · · · · · 6/38
  5. Read Individual Resources API Examples ·•········ @ a p i

    . r o u t e ( ' / s t u d e n t s / < i n t : i d > ' , m e t h o d s = [ ' G E T ' ] ) d e f g e t _ s t u d e n t ( i d ) : s = S t u d e n t . q u e r y . g e t _ o r _ 4 0 4 ( i d ) r e t u r n j s o n i f y ( s . t o _ j s o n ( ) ) G E T requests are also used to read individual resources. Routes can match dynamic components, such as the resource id. Errors are handled in lower-level code with exceptions, making high-level code straightforward and clean. The t o _ j s o n ( ) helper method in the model class returns the JSON representation as a Python dictionary. A successful response has status code 2 0 0 ( O K ) . · · · · · 7/38
  6. Per-Resource Collections API Examples ··•······· f r o m f

    l a s k i m p o r t j s o n i f y @ a p i . r o u t e ( ' / s t u d e n t s / < i n t : i d > / r e g i s t r a t i o n s / ' , m e t h o d s = [ ' G E T ' ] ) d e f g e t _ s t u d e n t _ r e g i s t r a t i o n s ( i d ) : s = S t u d e n t . q u e r y . g e t _ o r _ 4 0 4 ( i d ) r e t u r n j s o n i f y ( { ' u r l s ' : [ r e g . g e t _ u r l ( ) f o r r e g i n s . r e g i s t r a t i o n s . a l l ( ) ] } ) Logical subsets of resources (such as the class registrations of a student) are also exposed as collections. · 8/38
  7. Create a Resource API Examples ···•······ f r o m

    f l a s k i m p o r t j s o n i f y , r e q u e s t , u r l _ f o r @ a p i . r o u t e ( ' / s t u d e n t s / ' , m e t h o d s = [ ' P O S T ' ] ) d e f n e w _ s t u d e n t ( ) : s = S t u d e n t ( ) . f r o m _ j s o n ( r e q u e s t . j s o n ) d b . s e s s i o n . a d d ( s ) d b . s e s s i o n . c o m m i t ( ) r e s p o n s e = j s o n i f y ( { } ) r e s p o n s e . s t a t u s _ c o d e = 2 0 1 r e s p o n s e . h e a d e r s [ ' L o c a t i o n ' ] = s . g e t _ u r l ( ) r e t u r n r e s p o n s e P O S T requests are used to create a new resource. The f r o m _ j s o n ( ) helper method imports the JSON data from the request. A successful response has a status code 2 0 1 ( C r e a t e d ) and a L o c a t i o n header with the URI of the new resource. · · · 9/38
  8. Update a Resource API Examples ····•····· f r o m

    f l a s k i m p o r t j s o n i f y @ a p i . r o u t e ( ' / s t u d e n t s / < i n t : i d > ' , m e t h o d s = [ ' P U T ' ] ) d e f e d i t _ s t u d e n t ( i d ) : s = S t u d e n t . q u e r y . g e t _ o r _ 4 0 4 ( i d ) s . f r o m _ j s o n ( r e q u e s t . j s o n ) d b . s e s s i o n . a d d ( s ) d b . s e s s i o n . c o m m i t ( ) r e t u r n j s o n i f y ( { } ) P U T requests are used to modify existing resources. Error checking is pushed into lower-level code to keep high-level code clean. The f r o m _ j s o n ( ) helper is used to update the model from the JSON representation. A successful response has status code 2 0 0 ( O K ) . · · · · 10/38
  9. Delete a Resource API Examples ·····•···· f r o m

    f l a s k i m p o r t j s o n i f y @ a p i . r o u t e ( ' / s t u d e n t s / < i n t : i d > ' , m e t h o d s = [ ' D E L E T E ' ] ) d e f d e l e t e _ s t u d e n t ( i d ) : s = S t u d e n t . q u e r y . g e t _ o r _ 4 0 4 ( i d ) d b . s e s s i o n . d e l e t e ( s ) d b . s e s s i o n . c o m m i t ( ) r e t u r n j s o n i f y ( { } ) D E L E T E requests are used to remove resources. A successful response has status code 2 0 0 ( O K ) . · · 11/38
  10. Exporting JSON Representations API Examples ······•··· c l a s

    s S t u d e n t ( d b . M o d e l ) : # . . . d e f g e t _ u r l ( s e l f ) : r e t u r n u r l _ f o r ( ' a p i . g e t _ s t u d e n t ' , i d = s e l f . i d , _ e x t e r n a l = T r u e ) d e f t o _ j s o n ( s e l f ) : r e t u r n { ' u r l ' : s e l f . g e t _ u r l ( ) , ' n a m e ' : s e l f . n a m e , ' r e g i s t r a t i o n s ' : u r l _ f o r ( ' a p i . g e t _ s t u d e n t _ r e g i s t r a t i o n s ' , i d = s e l f . i d , _ e x t e r n a l = T r u e ) } Models generate their JSON representations. Flask's u r l _ f o r ( ) generates the links to related resources. · · 12/38
  11. Importing JSON Representations API Examples ·······•·· c l a s

    s V a l i d a t i o n E r r o r ( V a l u e E r r o r ) : p a s s c l a s s S t u d e n t ( d b . M o d e l ) : # . . . d e f f r o m _ j s o n ( s e l f , j s o n ) : t r y : s e l f . n a m e = j s o n [ ' n a m e ' ] e x c e p t K e y E r r o r a s e : r a i s e V a l i d a t i o n E r r o r ( ' I n v a l i d s t u d e n t : m i s s i n g ' + e . a r g s [ 0 ] ) r e t u r n s e l f Models update themselves from their JSON representation. In case of errors such as missing keys, an exception is raised. · · 13/38
  12. Error Handling API Examples ········•· d e f b a

    d _ r e q u e s t ( m e s s a g e ) : r e s p o n s e = j s o n i f y ( { ' e r r o r ' : ' b a d r e q u e s t ' , ' m e s s a g e ' : m e s s a g e } ) r e s p o n s e . s t a t u s _ c o d e = 4 0 0 r e t u r n r e s p o n s e d e f n o t _ f o u n d _ e r r o r ( e ) : r e s p o n s e = j s o n i f y ( { ' e r r o r ' : ' n o t f o u n d ' , ' m e s s a g e ' : m e s s a g e } ) r e s p o n s e . s t a t u s _ c o d e = 4 0 4 r e t u r n r e s p o n s e @ a p i . e r r o r h a n d l e r ( V a l i d a t i o n E r r o r ) d e f v a l i d a t i o n _ e r r o r ( e ) : r e t u r n b a d _ r e q u e s t ( e . a r g s [ 0 ] ) @ a p i . e r r o r h a n d l e r ( 4 0 4 ) d e f n o t _ f o u n d _ e r r o r ( e ) : r e t u r n n o t _ f o u n d ( e . a r g s [ 0 ] ) High-level code in routes is straightforward, with little or no error checking. Helper functions generate all the possible error responses. Most errors are raised from lower-level code in decorators, models or service layer. Exceptions are caught with Flask's e r r o r h a n d l e r decorator, and the proper response is returned. · · · · 14/38
  13. Unit Testing API Examples ·········• c l a s s

    T e s t A P I ( u n i t t e s t . T e s t C a s e ) : # . . . d e f t e s t _ s t u d e n t s ( s e l f ) : # c r e a t e n e w r v , j s o n = s e l f . c l i e n t . p o s t ( ' / s t u d e n t s / ' , d a t a = { ' n a m e ' : ' s u s a n ' } ) s e l f . a s s e r t T r u e ( r v . s t a t u s _ c o d e = = 2 0 1 ) s u s a n _ u r l = r v . h e a d e r s [ ' L o c a t i o n ' ] # g e t r v , j s o n = s e l f . c l i e n t . g e t ( s u s a n _ u r l ) s e l f . a s s e r t T r u e ( r v . s t a t u s _ c o d e = = 2 0 0 ) s e l f . a s s e r t T r u e ( j s o n [ ' n a m e ' ] = = ' s u s a n ' ) s e l f . a s s e r t T r u e ( j s o n [ ' u r l ' ] = = s u s a n _ u r l ) The testing API client is built on top of Flask's own test client. · 15/38
  14. Flask-HTTPAuth Advanced APIs with Flask f r o m f

    l a s k i m p o r t g f r o m f l a s k . e x t . h t t p a u t h i m p o r t H T T P B a s i c A u t h a u t h = H T T P B a s i c A u t h ( ) @ a u t h . v e r i f y _ p a s s w o r d d e f v e r i f y _ p a s s w o r d ( u s e r n a m e , p a s s w o r d ) : g . u s e r = U s e r . q u e r y . f i l t e r _ b y ( u s e r n a m e = u s e r n a m e ) . f i r s t ( ) r e t u r n g . u s e r i s n o t N o n e a n d g . u s e r . v e r i f y _ p a s s w o r d ( p a s s w o r d ) The Flask-HTTPAuth extension handles the HTTP Basic Auth protocol. The a u t h . v e r i f y _ p a s s w o r d decorator registers a password verification function, invoked by Flask-HTTPAuth before protected routes are called. The user model contains the password hashing and verification logic. The authenticated user is passed to route functions via the g context global. · · · · 18/38
  15. Protecting API Routes Advanced APIs with Flask @ a p

    i . r o u t e ( ' / s t u d e n t s / < i n t : i d > ' , m e t h o d s = [ ' P U T ' ] ) @ a u t h . l o g i n _ r e q u i r e d d e f e d i t _ s t u d e n t ( i d ) : s = S t u d e n t . q u e r y . g e t _ o r _ 4 0 4 ( i d ) s . f r o m _ j s o n ( r e q u e s t . j s o n ) d b . s e s s i o n . a d d ( s ) d b . s e s s i o n . c o m m i t ( ) r e t u r n j s o n i f y ( { } ) The a u t h . l o g i n _ r e q u i r e d decorator protects a route. Bad or missing credentials return a 4 0 1 ( U n a u t h o r i z e d ) response. · · 19/38
  16. Global API Protection Advanced APIs with Flask @ a p

    i . b e f o r e _ r e q u e s t @ a u t h . l o g i n _ r e q u i r e d d e f b e f o r e _ r e q u e s t ( ) : # a u t h e n t i c a t e d u s e r i s a v a i l a b l e h e r e a n d i n r o u t e s a s g . u s e r p a s s Flask's b e f o r e _ r e q u e s t decorator registers a function that runs before requests in the blueprint are dispatched. If all the routes in the API require authentication, then decorate the b e f o r e _ r e q u e s t handler. · · 20/38
  17. Using Authentication Tokens Advanced APIs with Flask @ a u

    t h . v e r i f y _ p a s s w o r d d e f v e r i f y _ p a s s w o r d ( t o k e n , p a s s w o r d ) : g . u s e r = U s e r . v a l i d a t e _ a u t h _ t o k e n ( t o k e n ) r e t u r n g . u s e r i s n o t N o n e The user model generates and validates authentication tokens. There are many ways to generate tokens, one possibility is i t s d a n g e r o u s . The v e r i f y _ p a s s w o r d decorated function validates tokens given in the username field. The password field is not used. Tokens can be given to clients through template rendering or through a password-protected route. · · · · 21/38
  18. JSON Output Advanced APIs with Flask @ t o k

    e n . r o u t e ( ' / r e q u e s t - t o k e n ' ) @ t o k e n _ a u t h . l o g i n _ r e q u i r e d @ j s o n d e f r e q u e s t _ t o k e n ( ) : r e t u r n { ' t o k e n ' : g . u s e r . g e n e r a t e _ a u t h _ t o k e n ( ) } @ a p i . r o u t e ( ' / s t u d e n t s / < i n t : i d > ' , m e t h o d s = [ ' G E T ' ] ) @ j s o n d e f g e t _ s t u d e n t ( i d ) : r e t u r n S t u d e n t . q u e r y . g e t _ o r _ 4 0 4 ( i d ) The j s o n i f y ( ) and t o _ j s o n ( ) functions are used a lot. A custom decorator can make these calls for us! · · 23/38
  19. JSON Decorator Implementation Advanced APIs with Flask d e f

    j s o n ( f ) : @ f u n c t o o l s . w r a p s ( f ) d e f w r a p p e d ( * a r g s , * * k w a r g s ) : r v = f ( * a r g s , * * k w a r g s ) i f n o t i s i n s t a n c e ( r v , d i c t ) : r v = r v . t o _ j s o n ( ) r e t u r n j s o n i f y ( r v ) r e t u r n w r a p p e d 24/38
  20. Rate Limiting Advanced APIs with Flask f r o m

    f l a s k i m p o r t j s o n i f y @ a p i . r o u t e ( ' / s t u d e n t s / ' , m e t h o d s = [ ' G E T ' ] ) @ r a t e _ l i m i t ( l i m i t = 5 , p e r = 1 5 ) # m a x i m u m o f 5 r e q u e s t s p e r 1 5 s e c o n d s d e f g e t _ s t u d e n t s ( ) : r e t u r n j s o n i f y ( { ' u r l s ' : [ s . g e t _ u r l ( ) f o r s i n S t u d e n t . q u e r y . a l l ( ) ] } ) The rate limiting logic is wrapped in a custom decorator. The limits are enforced individually for each client based on IP address. The decorator can be applied individually to routes, or globally to a b e f o r e _ r e q u e s t handler. · · · 26/38
  21. Rate Limiting Decorator Advanced APIs with Flask d e f

    r a t e _ l i m i t ( l i m i t , p e r ) : d e f d e c o r a t o r ( f ) : @ f u n c t o o l s . w r a p s ( f ) d e f w r a p p e d ( * a r g s , * * k w a r g s ) : k e y = ' r a t e - l i m i t / % s / % s / ' % ( f . _ _ n a m e _ _ , r e q u e s t . r e m o t e _ a d d r ) l i m i t e r = R a t e L i m i t ( k e y , l i m i t , p e r ) i f n o t l i m i t e r . o v e r _ l i m i t : r e t u r n f ( * a r g s , * * k w a r g s ) r e t u r n t o o _ m a n y _ r e q u e s t s ( ' Y o u h a v e e x c e e d e d y o u r r e q u e s t r a t e ' ) r e t u r n w r a p p e d r e t u r n d e c o r a t o r The R a t e L i m i t class uses Redis for storage. The full implementation is included in the example code. A request that is above the limit is returned with the 4 2 9 ( T o o M a n y R e q u e s t s ) status code. · · 27/38
  22. Paginating Collection of Resources Advanced APIs with Flask @ a

    p i . r o u t e ( ' / s t u d e n t s / < i n t : i d > / r e g i s t r a t i o n s / ' , m e t h o d s = [ ' G E T ' ] ) @ p a g i n a t e ( ) d e f g e t _ s t u d e n t _ r e g i s t r a t i o n s ( i d ) : s = S t u d e n t . q u e r y . g e t _ o r _ 4 0 4 ( i d ) r e t u r n s . r e g i s t r a t i o n s Large collections are paginated to avoid large queries. The pagination boilerplate is hidden in a custom decorator. The route function returns the database query object and the decorator does the rest! · · · 29/38
  23. Paginated Responses Advanced APIs with Flask G E T h

    t t p : / / e x a m p l e . c o m / a p i / v 1 . 0 / s t u d e n t s / 1 / r e g i s t r a t i o n s / { " u r l s " : [ " h t t p : / / e x a m p l e . c o m / a p i / v 1 . 0 / r e g i s t r a t i o n s / 1 " , " h t t p : / / e x a m p l e . c o m / a p i / v 1 . 0 / r e g i s t r a t i o n s / 5 " , . . . " h t t p : / / e x a m p l e . c o m / a p i / v 1 . 0 / r e g i s t r a t i o n s / 1 2 5 " ] , " m e t a " : { " p a g e " : 1 , " p e r _ p a g e " : 1 0 , " t o t a l " : 1 2 , " p a g e s " : 2 , " p r e v " : n u l l , " n e x t " : " h t t p : / / e x a m p l e . c o m / a p i / v 1 . 0 / r e g i s t r a t i o n s / ? p a g e = 2 & p e r _ p a g e = 1 0 " , " f i r s t " : " h t t p : / / e x a m p l e . c o m / a p i / v 1 . 0 / r e g i s t r a t i o n s / ? p a g e = 1 & p e r _ p a g e = 1 0 " , " l a s t " : " h t t p : / / e x a m p l e . c o m / a p i / v 1 . 0 / r e g i s t r a t i o n s / ? p a g e = 2 & p e r _ p a g e = 1 0 " } } Page information is included in the JSON response. Clients can build the navigation links with this information. · · 30/38
  24. Pagination Decorator Advanced APIs with Flask d e f p

    a g i n a t e ( m a x _ p e r _ p a g e = 1 0 ) : d e f d e c o r a t o r ( f ) : @ f u n c t o o l s . w r a p s ( f ) d e f w r a p p e d ( * a r g s , * * k w a r g s ) : p a g e = r e q u e s t . a r g s . g e t ( ' p a g e ' , 1 , t y p e = i n t ) p e r _ p a g e = m i n ( r e q u e s t . a r g s . g e t ( ' p e r _ p a g e ' , m a x _ p e r _ p a g e , t y p e = i n t ) , m a x _ p e r _ p a g e ) q u e r y = f ( * a r g s , * * k w a r g s ) p = q u e r y . p a g i n a t e ( p a g e , p e r _ p a g e ) p a g e s = { ' p a g e ' : p a g e , ' p e r _ p a g e ' : p e r _ p a g e , ' t o t a l ' : p . t o t a l , ' p a g e s ' : p . p a g e s } i f p . h a s _ p r e v : p a g e s [ ' p r e v ' ] = u r l _ f o r ( r e q u e s t . e n d p o i n t , p a g e = p . p r e v _ n u m , p e r _ p a g e = p e r _ p a g e , * * k w a r g s ) i f p . h a s _ n e x t : p a g e s [ ' n e x t ' ] = u r l _ f o r ( r e q u e s t . e n d p o i n t , p a g e = p . n e x t _ n u m , p e r _ p a g e = p e r _ p a g e , * * k w a r g s ) p a g e s [ ' f i r s t ' ] = u r l _ f o r ( r e q u e s t . e n d p o i n t , p a g e = 1 , p e r _ p a g e = p e r _ p a g e , * * k w a r g s ) p a g e s [ ' l a s t ' ] = u r l _ f o r ( r e q u e s t . e n d p o i n t , p a g e = p . p a g e s , p e r _ p a g e = p e r _ p a g e , * * k w a r g s ) r e t u r n j s o n i f y ( { ' u r l s ' : [ i t e m . g e t _ u r l ( ) f o r i t e m i n p . i t e m s ] , ' m e t a ' : p a g e s } ) r e t u r n w r a p p e d r e t u r n d e c o r a t o r This decorator implementation expects routes to return a Flask-SQLAlchemy query object. The page number and the page size are given in the query string. · · 31/38
  25. Adding Cache Directives Advanced APIs with Flask @ t o

    k e n . r o u t e ( ' / r e q u e s t - t o k e n ' ) @ a u t h . l o g i n _ r e q u i r e d @ n o _ c a c h e @ j s o n d e f r e q u e s t _ t o k e n ( ) : r e t u r n { ' t o k e n ' : g . u s e r . g e n e r a t e _ a u t h _ t o k e n ( ) } Some responses should never be cached. The n o _ c a c h e decorator adds the appropriate C a c h e - C o n t r o l header to the response. · · 33/38
  26. Cache Control Decorator Advanced APIs with Flask d e f

    c a c h e _ c o n t r o l ( * d i r e c t i v e s ) : d e f d e c o r a t o r ( f ) : @ f u n c t o o l s . w r a p s ( f ) d e f w r a p p e d ( * a r g s , * * k w a r g s ) : r v = f ( * a r g s , * * k w a r g s ) r v = m a k e _ r e s p o n s e ( r v ) r v . h e a d e r s [ ' C a c h e - C o n t r o l ' ] = ' , ' . j o i n ( d i r e c t i v e s ) r e t u r n r v r e t u r n w r a p p e d r e t u r n d e c o r a t o r d e f n o _ c a c h e ( f ) : r e t u r n c a c h e _ c o n t r o l ( ' n o - c a c h e ' , ' n o - s t o r e ' , ' m a x - a g e = 0 ' ) ( f ) The c a c h e _ c o n t r o l header takes HTTP caching directives as arguments and adds them to the response. The n o - c a c h e decorator provides a convenient shortcut. · · 34/38
  27. ETag Headers Advanced APIs with Flask @ a p i

    . r o u t e ( ' / s t u d e n t s / ' , m e t h o d s = [ ' G E T ' ] ) @ e t a g @ p a g i n a t e ( ) d e f g e t _ s t u d e n t s ( ) : r e t u r n S t u d e n t . q u e r y HTTP caches can use "entity tags" or "etags" to identify resources. If the etag changes the client knows the version of the resource it has is stale. The e t a g custom decorator generates etags for responses to G E T requests and handles the conditional requests that use them. · · · 36/38
  28. ETag Decorator Advanced APIs with Flask d e f e

    t a g ( f ) : @ f u n c t o o l s . w r a p s ( f ) d e f w r a p p e d ( * a r g s , * * k w a r g s ) : # o n l y f o r H E A D a n d G E T r e q u e s t s a s s e r t r e q u e s t . m e t h o d i n [ ' H E A D ' , ' G E T ' ] , ' @ e t a g i s o n l y s u p p o r t e d f o r G E T r e q u e s t s ' r v = f ( * a r g s , * * k w a r g s ) r v = m a k e _ r e s p o n s e ( r v ) e t a g = ' " ' + h a s h l i b . m d 5 ( r v . g e t _ d a t a ( ) ) . h e x d i g e s t ( ) + ' " ' r v . h e a d e r s [ ' E T a g ' ] = e t a g i f _ m a t c h = r e q u e s t . h e a d e r s . g e t ( ' I f - M a t c h ' ) i f _ n o n e _ m a t c h = r e q u e s t . h e a d e r s . g e t ( ' I f - N o n e - M a t c h ' ) i f i f _ m a t c h : e t a g _ l i s t = [ t a g . s t r i p ( ) f o r t a g i n i f _ m a t c h . s p l i t ( ' , ' ) ] i f e t a g n o t i n e t a g _ l i s t a n d ' * ' n o t i n e t a g _ l i s t : r v = p r e c o n d i t i o n _ f a i l e d ( ) e l i f i f _ n o n e _ m a t c h : e t a g _ l i s t = [ t a g . s t r i p ( ) f o r t a g i n i f _ n o n e _ m a t c h . s p l i t ( ' , ' ) ] i f e t a g i n e t a g _ l i s t o r ' * ' i n e t a g _ l i s t : r v = n o t _ m o d i f i e d ( ) r e t u r n r v r e t u r n w r a p p e d The e t a g decorator adds the MD5 of the response as the E T a g header. · 37/38