diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2012-09-27 03:33:00 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2012-09-27 03:33:00 (GMT) |
commit | 81c0fce84135786d06e9a84c4efff60b2e3f491d (patch) | |
tree | 5b3433b53010de8ab48679baf9e60af760ae85fb | |
parent | b301632857c7376d03d18df502682a077e9539db (diff) |
Process Last-Modified/If-Modified-Since HTTP headers from Router
-rw-r--r-- | sugar_network/toolkit/http.py | 115 | ||||
-rw-r--r-- | sugar_network/toolkit/router.py | 45 | ||||
-rwxr-xr-x | tests/units/context.py | 8 | ||||
-rwxr-xr-x | tests/units/router.py | 98 |
4 files changed, 200 insertions, 66 deletions
diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index 37a5388..368cd08 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -69,27 +69,65 @@ class Client(object): def get(self, path_=None, **kwargs): kwargs.update(self.params) - return self.request('GET', path_, params=kwargs) + response = self.request('GET', path_, params=kwargs) + return self._decode_response(response) def post(self, path_=None, data_=None, **kwargs): kwargs.update(self.params) - return self.request('POST', path_, data_, + response = self.request('POST', path_, data_, headers={'Content-Type': 'application/json'}, params=kwargs) + return self._decode_response(response) def put(self, path_=None, data_=None, **kwargs): kwargs.update(self.params) - return self.request('PUT', path_, data_, + response = self.request('PUT', path_, data_, headers={'Content-Type': 'application/json'}, params=kwargs) + return self._decode_response(response) def delete(self, path_=None, **kwargs): kwargs.update(self.params) - return self.request('DELETE', path_, params=kwargs) + response = self.request('DELETE', path_, params=kwargs) + return self._decode_response(response) + + def request(self, method, path=None, data=None, headers=None, allowed=None, + **kwargs): + if not path: + path = [''] + if not isinstance(path, basestring): + path = '/'.join([i.strip('/') for i in [self.api_url] + path]) + + if data is not None and headers and \ + headers.get('Content-Type') == 'application/json': + data = json.dumps(data) + + while True: + try: + response = requests.request(method, path, data=data, + headers=headers, session=self._session, **kwargs) + except requests.exceptions.SSLError: + _logger.warning('Use --no-check-certificate to avoid checks') + raise + + if response.status_code != 200: + if response.status_code == 401: + enforce(self._sugar_auth, + 'Operation is not available in anonymous mode') + _logger.info('User is not registered on the server, ' + 'registering') + self._register() + continue + if allowed and response.status_code in allowed: + return response + content = response.content + try: + error = json.loads(content) + except Exception: + _logger.debug('Got %s HTTP error for %r request:\n%s', + response.status_code, path, content) + response.raise_for_status() + else: + raise RuntimeError(error['error']) - def request(self, method, path=None, data=None, headers=None, **kwargs): - response = self._request(method, path, data, headers, **kwargs) - if response.headers.get('Content-Type') == 'application/json': - return json.loads(response.content) - else: return response def call(self, request): @@ -105,8 +143,9 @@ class Client(object): if prop: path.append(prop) - return self.request(method, path, data=request.content, params=params, - headers={'Content-Type': 'application/json'}) + response = self.request(method, path, data=request.content, + params=params, headers={'Content-Type': 'application/json'}) + return self._decode_response(response) def download(self, url_path, out_path, seqno=None, extract=False): if isdir(out_path): @@ -118,7 +157,7 @@ class Client(object): if seqno: params['seqno'] = seqno - response = self._request('GET', url_path, allow_redirects=True, + response = self.request('GET', url_path, allow_redirects=True, params=params, allowed=[404]) if response.status_code != 200: return 'application/octet-stream' @@ -176,54 +215,14 @@ class Client(object): return mime_type def subscribe(self): - response = self.request('GET', params={'cmd': 'subscribe'}) + response = self._decode_response( + self.request('GET', params={'cmd': 'subscribe'})) for line in _readlines(response.raw): if line.startswith('data: '): yield json.loads(line.split(' ', 1)[1]) - def _request(self, method, path, data=None, headers=None, allowed=None, - **kwargs): - if not path: - path = [''] - if not isinstance(path, basestring): - path = '/'.join([i.strip('/') for i in [self.api_url] + path]) - - if data is not None and headers and \ - headers.get('Content-Type') == 'application/json': - data = json.dumps(data) - - while True: - try: - response = requests.request(method, path, data=data, - headers=headers, session=self._session, **kwargs) - except requests.exceptions.SSLError: - _logger.warning('Use --no-check-certificate to avoid checks') - raise - - if response.status_code != 200: - if response.status_code == 401: - enforce(self._sugar_auth, - 'Operation is not available in anonymous mode') - _logger.info('User is not registered on the server, ' - 'registering') - self._register() - continue - if allowed and response.status_code in allowed: - return response - content = response.content - try: - error = json.loads(content) - except Exception: - _logger.debug('Got %s HTTP error for %r request:\n%s', - response.status_code, path, content) - response.raise_for_status() - else: - raise RuntimeError(error['error']) - - return response - def _register(self): - self._request('POST', ['user'], + self.request('POST', ['user'], headers={ 'Content-Type': 'application/json', }, @@ -236,6 +235,12 @@ class Client(object): }, ) + def _decode_response(self, response): + if response.headers.get('Content-Type') == 'application/json': + return json.loads(response.content) + else: + return response + def _sign(key_path, data): key = DSA.load_key(key_path) diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index 58478c7..a30a7f6 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -16,8 +16,10 @@ import os import cgi import json +import time import types import logging +from email.utils import parsedate, formatdate from urlparse import parse_qsl, urlsplit from bisect import bisect_left from os.path import join, isfile @@ -125,6 +127,11 @@ class Router(object): if request.path[:1] == ['static']: static_path = join(static.PATH, *request.path[1:]) enforce(isfile(static_path), 'No such file') + mtime = os.stat(static_path).st_mtime + if request.if_modified_since and \ + mtime <= request.if_modified_since: + raise ad.NotModified() + response.last_modified = mtime result = file(static_path) else: rout = None @@ -163,6 +170,9 @@ class Router(object): response.status = '303 See Other' response['Location'] = error.location response.content_type = None + except ad.NotModified: + response.status = '304 Not Modified' + response.content_type = None except Exception, error: util.exception('Error while processing %r request', request.url) @@ -285,6 +295,13 @@ class _Request(Request): 'Multipart request should contain only one file') self.content_stream = files.list[0].file + if_modified_since = environ.get('HTTP_IF_MODIFIED_SINCE') + if if_modified_since: + if_modified_since = parsedate(if_modified_since) + enforce(if_modified_since is not None, + 'Failed to parse If-Modified-Since') + self.if_modified_since = time.mktime(if_modified_since) + scope = len(self.path) enforce(scope >= 0 and scope < 4, BadRequest, 'Incorrect requested path') @@ -305,24 +322,33 @@ class _Request(Request): class _Response(ad.Response): + # pylint: disable-msg=E0202 status = '200 OK' - def get_content_length(self): + @property + def content_length(self): return self.get('Content-Length') - def set_content_length(self, value): + @content_length.setter + def content_length(self, value): self['Content-Length'] = value - content_length = property(get_content_length, set_content_length) - - def get_content_type(self): + @property + def content_type(self): return self.get('Content-Type') - def set_content_type(self, value): + @content_type.setter + def content_type(self, value): self['Content-Type'] = value - content_type = property(get_content_type, set_content_type) + @property + def last_modified(self): + return self.get('Last-Modified') + + @last_modified.setter + def last_modified(self, value): + self['Last-Modified'] = formatdate(value, localtime=False, usegmt=True) def items(self): for key, value in dict.items(self): @@ -332,6 +358,11 @@ class _Response(ad.Response): else: yield key, str(value) + def __repr__(self): + args = ['status=%r' % self.status, + ] + ['%s=%r' % i for i in self.items()] + return '<active_document.Response %s>' % ' '.join(args) + def _parse_accept_language(accept_language): if not accept_language: diff --git a/tests/units/context.py b/tests/units/context.py index e9ea911..9fb2b4a 100755 --- a/tests/units/context.py +++ b/tests/units/context.py @@ -159,16 +159,16 @@ class ContextTest(tests.Test): self.assertEqual( {'total': 2, 'result': [{'arch': 'x86', 'name': 'Gentoo-2.1'}, {'arch': 'x86_64', 'name': 'Debian-6.0'}]}, - client.request('GET', ['packages'])) + client.get(['packages'])) self.assertEqual( {'total': 2, 'result': ['package1', 'package2']}, - client.request('GET', ['packages', 'Gentoo-2.1'])) + client.get(['packages', 'Gentoo-2.1'])) self.assertEqual( ['package1-1', 'package1-2'], - client.request('GET', ['packages', 'Gentoo-2.1', 'package1'])) + client.get(['packages', 'Gentoo-2.1', 'package1'])) self.assertEqual( ['package1-3'], - client.request('GET', ['packages', 'Debian-6.0', 'package1'])) + client.get(['packages', 'Debian-6.0', 'package1'])) self.assertRaises(RuntimeError, client.request, 'GET', ['packages', 'Debian-6.0', 'package2']) self.assertRaises(RuntimeError, client.request, 'GET', ['packages', 'Gentoo-2.1', 'package3']) diff --git a/tests/units/router.py b/tests/units/router.py index 0b65ac3..44333f2 100755 --- a/tests/units/router.py +++ b/tests/units/router.py @@ -6,6 +6,7 @@ import time import urllib2 import hashlib import tempfile +from email.utils import formatdate from cStringIO import StringIO from os.path import exists @@ -319,6 +320,103 @@ class RouterTest(tests.Test): client = Client('http://localhost:8800', sugar_auth=True) self.assertEqual('en', client.get(['testdocument', guid, 'prop'])) + def test_IfModifiedSince(self): + + class TestDocument(Document): + + @ad.active_property(slot=100, typecast=int) + def prop(self, value): + if not self.request.if_modified_since or self.request.if_modified_since >= value: + return value + else: + raise ad.NotModified() + + self.start_master([User, TestDocument]) + client = Client('http://localhost:8800', sugar_auth=True) + + guid = client.post(['testdocument'], {'prop': 10}) + self.assertEqual( + 200, + client.request('GET', ['testdocument', guid, 'prop']).status_code) + self.assertEqual( + 200, + client.request('GET', ['testdocument', guid, 'prop'], headers={ + 'If-Modified-Since': formatdate(11, localtime=False, usegmt=True), + }).status_code) + self.assertEqual( + 304, + client.request('GET', ['testdocument', guid, 'prop'], headers={ + 'If-Modified-Since': formatdate(9, localtime=False, usegmt=True), + }).status_code) + + def test_LastModified(self): + + class TestDocument(Document): + + @ad.active_property(slot=100, typecast=int) + def prop1(self, value): + self.request.response.last_modified = value + return value + + @ad.active_property(slot=101, typecast=int) + def prop2(self, value): + return value + + self.start_master([User, TestDocument]) + client = Client('http://localhost:8800', sugar_auth=True) + + guid = client.post(['testdocument'], {'prop1': 10, 'prop2': 20}) + self.assertEqual( + formatdate(10, localtime=False, usegmt=True), + client.request('GET', ['testdocument', guid, 'prop1']).headers['Last-Modified']) + mtime = os.stat('master/testdocument/%s/%s/prop2' % (guid[:2], guid)).st_mtime + self.assertEqual( + formatdate(mtime, localtime=False, usegmt=True), + client.request('GET', ['testdocument', guid, 'prop2']).headers['Last-Modified']) + + def test_StaticFiles(self): + + class TestDocument(Document): + pass + + self.start_master([User, TestDocument]) + client = Client('http://localhost:8800', sugar_auth=True) + guid = client.post(['testdocument'], {}) + + local_path = '../../../sugar_network/static/images/missing.png' + response = client.request('GET', ['static', 'images', 'missing.png']) + self.assertEqual(200, response.status_code) + assert file(local_path).read() == response.content + self.assertEqual( + formatdate(os.stat(local_path).st_mtime, localtime=False, usegmt=True), + response.headers['Last-Modified']) + + def test_StaticFilesIfModifiedSince(self): + + class TestDocument(Document): + pass + + self.start_master([User, TestDocument]) + client = Client('http://localhost:8800', sugar_auth=True) + guid = client.post(['testdocument'], {}) + + mtime = os.stat('../../../sugar_network/static/images/missing.png').st_mtime + self.assertEqual( + 304, + client.request('GET', ['static', 'images', 'missing.png'], headers={ + 'If-Modified-Since': formatdate(mtime, localtime=False, usegmt=True), + }).status_code) + self.assertEqual( + 200, + client.request('GET', ['static', 'images', 'missing.png'], headers={ + 'If-Modified-Since': formatdate(mtime - 1, localtime=False, usegmt=True), + }).status_code) + self.assertEqual( + 304, + client.request('GET', ['static', 'images', 'missing.png'], headers={ + 'If-Modified-Since': formatdate(mtime + 1, localtime=False, usegmt=True), + }).status_code) + class Document(ad.Document): |