PK!y#UU$django_conditional_views/__init__.py__version__ = '0.1.2' from .views import * from .elements import etag as etag_elements, last_modified as last_modified_elements __all__ = [__version__, 'ConditionalGetMixin', 'ConditionalGetTemplateViewMixin', 'ConditionalGetDetailViewMixin', 'ConditionalGetListViewMixin', 'etag_elements', 'last_modified_elements'] PK!-django_conditional_views/elements/__init__.pyPK!`E)django_conditional_views/elements/base.pyimport datetime from abc import ABC from typing import Optional from django.template.response import SimpleTemplateResponse class BaseElement(ABC): """Base class for classes that derive either Last-Modified or Etag from the view.""" view_class = None def value(self, *args, **kwargs): raise NotImplementedError def __call__(self, view, *args, **kwargs): if not isinstance(view, self.view_class): raise ValueError(f'This function is only compatible with {self.view_class} views.') return self.value(view, *args, **kwargs) class BaseLastModifiedElement(BaseElement, ABC): """Inherit to define an element that derives Last-Modified""" def value(self, view) -> Optional[datetime.datetime]: raise NotImplementedError class BasePreRenderEtagElement(BaseElement, ABC): """Inherit to define an element that derives a pre render ETag""" def value(self, view) -> Optional[str]: raise NotImplementedError class BaseEtagPostRenderElement(BaseElement, ABC): """Inherit to define an element that derives a post render ETag""" def value(self, view, response: SimpleTemplateResponse) -> Optional[str]: raise NotImplementedError PK!z%6[[)django_conditional_views/elements/etag.pyfrom typing import Optional from django.template.response import SimpleTemplateResponse from django.views.generic.base import TemplateResponseMixin from .base import BaseEtagPostRenderElement __all__ = ['RenderedContentEtag'] class RenderedContentEtag(BaseEtagPostRenderElement): """Returns the response.content to use for etag hashing.""" view_class = TemplateResponseMixin def value(self, view: TemplateResponseMixin, response: SimpleTemplateResponse) -> Optional[str]: if not response.is_rendered: response.render() return response.content PK! 2django_conditional_views/elements/last_modified.pyimport datetime import os from typing import Optional from django.core.exceptions import ImproperlyConfigured from django.template.loader import select_template from django.views.generic.base import TemplateResponseMixin from django.views.generic.detail import SingleObjectMixin from django.views.generic.list import MultipleObjectMixin from .base import BaseLastModifiedElement __all__ = ['TemplateLastModified', 'ObjectLastModified', 'QuerySetLastModified'] class TemplateLastModified(BaseLastModifiedElement): """Returns the last modified time of the template file. Note that on ListView's self.object_list = self.get_queryset() must have already been run, see the ConditionalGetListViewMixin for an example implementation. """ view_class = TemplateResponseMixin def value(self, view: TemplateResponseMixin) -> Optional[datetime.datetime]: t = select_template(view.get_template_names()) local_tz = datetime.datetime.now().astimezone().tzinfo return datetime.datetime.fromtimestamp(os.stat(t.origin.name).st_mtime, tz=local_tz) class ObjectLastModified(BaseLastModifiedElement): """Returns the value of a configurable last modified field on the model instance.""" view_class = SingleObjectMixin def __init__(self, last_modified_field: str = 'modified'): self.last_modified_field = last_modified_field def value(self, view: SingleObjectMixin) -> Optional[datetime.datetime]: obj = view.get_object() if not hasattr(view.model, self.last_modified_field): raise ImproperlyConfigured( f"{view.model} does not have a field named {self.last_modified_field}." ) return getattr(obj, self.last_modified_field) class QuerySetLastModified(BaseLastModifiedElement): """Returns the latest value of a configurable last modified field on the model queryset.""" view_class = MultipleObjectMixin def __init__(self, last_modified_field: str = 'modified'): self.last_modified_field = last_modified_field def value(self, view: MultipleObjectMixin) -> Optional[datetime.datetime]: if not hasattr(view.model, self.last_modified_field): raise ImproperlyConfigured( f"{view.model} does not have a field named {self.last_modified_field}." ) last_modified = view.get_queryset().order_by('-' + self.last_modified_field) \ .values_list(self.last_modified_field, flat=True).first() return last_modified PK!Ytss#django_conditional_views/helpers.pyfrom inspect import isclass def instantiate(seq, *args, **kwargs): """Replace all classes with instantiated versions and return the modified sequence. >>> instantiate([list, list((1, 2))], (3,4)) [[3, 4], [1, 2]] """ output = [] for x in seq: if isclass(x): x = x(*args, **kwargs) output.append(x) return output PK!Nj--!django_conditional_views/views.pyimport hashlib from calendar import timegm from datetime import datetime from functools import partial from typing import ClassVar, Optional, Type, Union from django.http import HttpRequest, HttpResponse from django.template.response import SimpleTemplateResponse from django.utils.cache import get_conditional_response from django.utils.http import http_date, quote_etag from django.views.generic import DetailView, ListView from django.views.generic.base import TemplateResponseMixin, View from .elements.base import BaseElement from .elements.etag import RenderedContentEtag from .elements.last_modified import ObjectLastModified, QuerySetLastModified, TemplateLastModified from .helpers import instantiate __all__ = ['ConditionalGetMixin', 'ConditionalGetTemplateViewMixin', 'ConditionalGetDetailViewMixin', 'ConditionalGetListViewMixin'] class ConditionalGetMixin(View): # language=rst prefix=" " """Conditional Request/Response aware mixin for View If a request is made with conditional request headers, such as If-Modified-Since_ or If-None-Match_, the conditional request will be evaluated by `django.utils.cache.get_conditional_response`_. If the condition of the request is met then then normal view response will be returned, and the Etag and Last-Modified headers will be set if those values could be computed. If the condition of the request is NOT met then either a `304 Not Modified`_ or a `412 Precondition Failed`_ response will be returned instead. .. _If-Modified-Since: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since .. _If-None-Match: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match .. _django.utils.cache.get_conditional_response: https://github.com/django/django/blob/master/django/utils/cache.py#L134 .. _304 Not Modified: https://tools.ietf.org/html/rfc7232#section-4.1 .. _412 Precondition Failed: https://tools.ietf.org/html/rfc7232#section-4.2 """ last_modified_elements: ClassVar[Union[BaseElement, Type[BaseElement]]] = None pre_render_etag_elements: ClassVar[Union[BaseElement, Type[BaseElement]]] = None post_render_etag_elements: ClassVar[Union[BaseElement, Type[BaseElement]]] = None def get_last_modified(self) -> Optional[datetime]: # language=rst prefix=" " """Derive a Last-Modified datetime for the view. Can potentially save a call to dispatch. :return: A datetime representing the last time the view content was modified, or None. """ if not self.last_modified_elements: return None elements = [x(self) for x in instantiate(self.last_modified_elements)] elements = [x for x in elements if x] if not elements: return None return max(elements) def get_pre_render_etag(self) -> Optional[str]: # language=rst prefix=" " """Derive an ETag before the response is rendered. Can potentially save a call to dispatch. :return: The calculated ETag or '' """ if not self.pre_render_etag_elements: return None elements = [x(self) for x in instantiate(self.pre_render_etag_elements)] return self._render_etag_elements(elements) def get_post_render_etag(self, response: SimpleTemplateResponse) -> Optional[str]: # language=rst prefix=" " """Derive an Etag after the response is rendered. :return: The calculated ETag or None """ if not self.post_render_etag_elements: return None if not hasattr(response, 'render'): return None # Render the response response.render() elements = [x(self, response) for x in instantiate(self.post_render_etag_elements)] return self._render_etag_elements(elements) @staticmethod def _render_etag_elements(elements): elements = [x for x in elements if x] elements = [x.encode() if isinstance(x, str) else x for x in elements] if not elements: return None return hashlib.md5(b''.join(elements)).hexdigest() def set_response_headers(self, response: SimpleTemplateResponse, etag: str = None, last_modified: Union[int, float, datetime] = None ) -> SimpleTemplateResponse: # language=rst prefix=" " """Sets the Etag and Last-Modified headers on the response Override if you want to change the final headers. :param response: The response to add the headers to. :param etag: The properly formatted etag to add to the header. :param last_modified: The datetime or timestamp integer to set as the last_modified date. :return: The response with the headers added. """ if etag: response['Etag'] = self._format_etag(etag) if last_modified: response['Last-Modified'] = self._format_last_modified(last_modified) return response def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: # language=rst prefix=" " """Conditional Request/Response aware wrapper for dispatch. Calls get_last_modified and get_pre_render_etag to compute the Last-Modified and Etag headers, and then compares those against the conditional request headers (if any) to determine whether to return a 304 or 412 response. If a 304 or 412 response will be sent the super().dispatch method will never be called, so any computation or side effects done there will not happen. Otherwise, the response will be rendered and get_post_render_etag will be called to try to get a post_render etag. Once again this will be compared against any conditional request headers to determine whether to send a 304 or 412 response. If no conditional response will be sent, the Last-Modified and Etag headers for the current response are set and the response is returned. """ last_modified = self._prep_last_modified(self.get_last_modified()) # 1. Try to get an etag from the pre_render_etag method etag = self._prep_etag(self.get_pre_render_etag()) # 2. Try to generate a conditional response given the last_modified and pre_render_etag. # If possible, return an abbreviated conditional response using the last_modified and # pre_render_etag instead of proceeding with dispatch. conditional_response = self._get_conditional_response(request, etag, last_modified) if conditional_response: return conditional_response # 3. call the super dispatch response = super().dispatch(request, *args, **kwargs) response = self.set_response_headers(response, etag=etag, last_modified=last_modified) # 4. Add a post render callback for post render etag generation if hasattr(response, 'add_post_render_callback'): response.add_post_render_callback( partial(self._post_render_callback, request, last_modified) ) return response def _post_render_callback(self, request, last_modified, response): # 1. If we do not yet have an etag, try to get one from the post_render_etag method etag = response.get('ETag') if not etag and not response.streaming: etag = self._format_etag(self.get_post_render_etag(response)) # 2. Once again try to generate a conditional response given the last_modified and # pre_render_etag. conditional_response = self._get_conditional_response(request, etag, last_modified) if conditional_response: return conditional_response # 3. Finally set the Etag headers response = self.set_response_headers(response, etag=etag, last_modified=last_modified) return response def _get_conditional_response(self, request, etag, last_modified): """Mimics ConditionalGetMiddleware""" return get_conditional_response( request, etag=etag, last_modified=last_modified, ) def _prep_etag(self, etag: Optional[str]): """Prep etag for the conditional check phase""" return self._format_etag(etag) @staticmethod def _format_etag(etag: Optional[str]): """Format etag for the http header""" if not etag: return None return quote_etag(etag) @staticmethod def _prep_last_modified(last_modified: Optional[datetime]): """Prep last_modified for the conditional check phase""" if not last_modified: return None return timegm(last_modified.utctimetuple()) def _format_last_modified(self, last_modified: Union[int, float, datetime, None]): """Format the results of get_last_modified for the http header""" if not last_modified: return None if isinstance(last_modified, datetime): last_modified = self._prep_last_modified(last_modified) return http_date(last_modified) class ConditionalGetTemplateViewMixin(TemplateResponseMixin, ConditionalGetMixin): # language=rst prefix=" " """ Conditional Request/Response aware mixin for TemplateView *Last-Modified:* Calculated from the the template last modified timestamp. *Etag:* Calculated from the rendered response. """ last_modified_elements = [TemplateLastModified] post_render_etag_elements = [RenderedContentEtag] class ConditionalGetDetailViewMixin(ConditionalGetMixin, DetailView): # language=rst prefix=" " """Conditional Request/Response aware mixin for DetailView *Last-Modified:* Calculated from the latest of the template last modified timestamp and a configurable field on the model object, default 'modified'. *Etag:* Calculated from the rendered response. """ last_modified_field: ClassVar[str] = 'modified' post_render_etag_elements = [RenderedContentEtag] def get_last_modified(self): # TemplateLastModified requires that object exist self.object = super().get_object() self.last_modified_elements = self.last_modified_elements or [] self.last_modified_elements.extend([TemplateLastModified, ObjectLastModified(self.last_modified_field)]) return super().get_last_modified() class ConditionalGetListViewMixin(ConditionalGetMixin, ListView): # language=rst prefix=" " """Conditional Request/Response aware mixin for ListView *Last-Modified:* Calculated from the latest of the template last modified timestamp and the model objects 'modified' field. *Etag:* Calculated from the rendered response. """ last_modified_field: ClassVar[str] = 'modified' post_render_etag_elements = [RenderedContentEtag] def get_last_modified(self): # TemplateLastModified requires that object_list exist self.object_list = super().get_queryset() self.last_modified_elements = self.last_modified_elements or [] self.last_modified_elements.extend([TemplateLastModified, QuerySetLastModified(self.last_modified_field)]) return super().get_last_modified() PK!p..3django_conditional_views-0.1.2.dist-info/LICENSE.mdMIT License Copyright (c) 2018 Andrew Cordery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!HW"TT.django_conditional_views-0.1.2.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!H0!1django_conditional_views-0.1.2.dist-info/METADATAQKO1WQ`$a[ʘm]oƃnof"J+Y]a/hVpVRL=,}`?6.[Ơ?XJO50!%(1XcH$gΨB|1VA书׼p&/GQlHu^J09LRYA7Ƅ-gM^lqHo4~kplLHg/^WSWwD/?wڣ1duڊu ONͫw-fF|/0]5-PK!H#/django_conditional_views-0.1.2.dist-info/RECORDIP}y.zȬ( l Nc]{s9iy_:Ej?|?hU;c}P,ӭt~ LٶS3;Xn5^ޑ:#*`od-& {z!цεb]kJ1 %^_tUrX\Q;0[99&=b$EqopH bӉpOw&wGl B-&䩸2-釸jR!H0'gQmɽQi%]JX*୅] %аMu8N;UZ8@GG :hߗ 9 )nIG׻^f9@+&jF7 Ċ\Q%rU~b5)6;Z_v4F4ͧ3?16F{ 4Y~2W-ZXIZdeftNc-eLY1dOgZ~G/[)wPteɯGN=1oY; PK!y#UU$django_conditional_views/__init__.pyPK!-django_conditional_views/elements/__init__.pyPK!`E)django_conditional_views/elements/base.pyPK!z%6[[)django_conditional_views/elements/etag.pyPK! 2 django_conditional_views/elements/last_modified.pyPK!Ytss#django_conditional_views/helpers.pyPK!Nj--!xdjango_conditional_views/views.pyPK!p..3Bdjango_conditional_views-0.1.2.dist-info/LICENSE.mdPK!HW"TT.