Skip to content

Commit

Permalink
Introducing must_revalidate decorator, Closes #30
Browse files Browse the repository at this point in the history
  • Loading branch information
pylover committed Jul 12, 2017
1 parent ebff5df commit 1dadb74
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 3 deletions.
2 changes: 1 addition & 1 deletion nanohttp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
HttpMethodNotAllowed, HttpConflict, HttpGone, HttpRedirect, HttpMovedPermanently, HttpFound, \
HttpInternalServerError
from .controllers import Controller, RestController, Static
from .decorators import action, html, json, xml, binary, text
from .decorators import action, html, json, xml, binary, text, must_revalidate
from .helpers import quickstart, LazyAttribute
from .cli import main
from .contexts import context, ContextIsNotInitializedError
Expand Down
10 changes: 10 additions & 0 deletions nanohttp/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ def encode_response(self, buffer):
except AttributeError: # pragma: no cover
raise TypeError('The returned response should has the `encode` attribute, such as `str`.')

def expired(self, etag, force=False):
none_match = self.environ.get('HTTP_IF_NONE_MATCH')
match = self.environ.get('HTTP_IF_MATCH')
expired = etag not in (none_match, match)

if force and not expired:
raise exceptions.HttpNotModified()

return expired


class ContextProxy(Context):

Expand Down
14 changes: 14 additions & 0 deletions nanohttp/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import functools

from .configuration import settings
from .contexts import context


def action(*args, verbs='any', encoding='utf-8', content_type=None, inner_decorator=None, **kwargs):
Expand Down Expand Up @@ -48,3 +49,16 @@ def wrapper(*args, **kwargs):
json = functools.partial(action, content_type='application/json', inner_decorator=jsonify)
xml = functools.partial(action, content_type='application/xml')
binary = functools.partial(action, content_type='application/octet-stream', encoding=None)


def must_revalidate(etag):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
etag_ = etag() if callable(etag) else etag
context.expired(etag_, force=True)
context.response_headers.add_header('Cache-Control', 'must-revalidate')
context.response_headers.add_header('ETag', etag_)
return func(*args, **kwargs)
return wrapper
return decorator
8 changes: 6 additions & 2 deletions nanohttp/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ def __init__(self, location, *args, **kw):


class HttpMovedPermanently(HttpRedirect):
status_code, status_text, info = 301, 'Moved Permanently', 'Object moved permanently -- see URI list'
status_code, status_text, info = 301, 'Moved Permanently', 'Object moved permanently'


class HttpFound(HttpRedirect):
status_code, status_text, info = 302, 'Found', 'Object moved temporarily -- see URI list'
status_code, status_text, info = 302, 'Found', 'Object moved temporarily'


class HttpNotModified(HttpStatus):
status_code, status_text, info = 304, 'Not Modified', 'Resource is not modified'


class HttpInternalServerError(HttpStatus):
Expand Down
44 changes: 44 additions & 0 deletions nanohttp/tests/test_caching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import unittest

from nanohttp import Controller, text, must_revalidate
from nanohttp.tests.helpers import WsgiAppTestCase


_etag = '1'


class CachingTestCase(WsgiAppTestCase):

class Root(Controller):

@text()
@must_revalidate(etag=lambda: _etag)
def index(self):
yield 'Something'

@text()
def about(self):
yield 'about'

def test_caching_header(self):
global _etag
self.assert_get('/', expected_headers={'Cache-Control': 'must-revalidate', 'ETag': _etag})

# Fetching again with etag header
___, body = self.assert_get('/', headers={'If-None-Match': _etag}, status=304)
self.assertEqual(len(body), 0)

_old_etag = _etag
_etag = '2'
# Fetching again with etag header
___, body = self.assert_get(
'/',
headers={'If-None-Match': _old_etag},
status=200,
expected_headers={'Cache-Control': 'must-revalidate', 'ETag': _etag}
)
self.assertEqual(body, b'Something')


if __name__ == '__main__': # pragma: no cover
unittest.main()
5 changes: 5 additions & 0 deletions nanohttp/tests/test_redirect.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import unittest

from nanohttp import Controller, action, text, HttpMovedPermanently, HttpFound
from nanohttp.tests.helpers import WsgiAppTestCase

Expand All @@ -18,3 +20,6 @@ def test_redirect_response_header(self):
self.assert_get('/', status=301, expected_headers={'Location': '/new/address'})
self.assert_get('/about', status=302, expected_headers={'Location': '/new/address'})


if __name__ == '__main__': # pragma: no cover
unittest.main()

0 comments on commit 1dadb74

Please sign in to comment.