Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2012-09-27 03:33:00 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2012-09-27 03:33:00 (GMT)
commit81c0fce84135786d06e9a84c4efff60b2e3f491d (patch)
tree5b3433b53010de8ab48679baf9e60af760ae85fb
parentb301632857c7376d03d18df502682a077e9539db (diff)
Process Last-Modified/If-Modified-Since HTTP headers from Router
-rw-r--r--sugar_network/toolkit/http.py115
-rw-r--r--sugar_network/toolkit/router.py45
-rwxr-xr-xtests/units/context.py8
-rwxr-xr-xtests/units/router.py98
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):