feat(Response): Add header attributes for setting Last-Modified, Retry-After, Location, Content-Location, and Content-Range
Closes #7
This commit is contained in:
@@ -5,7 +5,7 @@ Falcon [](https://travis
|
||||
|
||||
**[Experimental]**
|
||||
|
||||
Falcon is a *really* fast, light-weight framework for building cloud APIs. It tries to do as little as possible while remaining highly effective.
|
||||
Falcon is a high-performance Python framework for building cloud APIs. It tries to do as little as possible while remaining highly effective.
|
||||
|
||||
> Perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away.
|
||||
>
|
||||
@@ -17,7 +17,9 @@ X is cool, and everyone is using it, so why mess with a good thing?
|
||||
|
||||
1. Unlike other Python web frameworks, Falcon won't bottleneck your API's performance. Most frameworks max out at serving simple "hello world" requests at a few thousand req/sec, while Falcon can easily serve 10 times as many on the same hardware. Even Bottle, one of the fastest frameworks we tested, takes twice as long to serve requests compared to Falcon.
|
||||
2. Falcon isn't very opinionated. In other words, the framework leaves a lot of decisions and implementation details to you, the API developer, which means you will need to use your head a little more than other frameworks, and probably write a little more code. On the other hand, this gives you a lot of freedom to customize and tune your implementation in order to create a solution that stands out from the crowd.
|
||||
3. I have no idea what you're talking about, but [here's a bunny][bunny] with a pancake on its head.
|
||||
3. Falcon doesn't include a lot of cruft that is simply unnecessary when building web services. Less code and fewer dependencies means a smaller attack surface, lower memory usage, and fewer places for bugs to hide.
|
||||
|
||||
If you're still not convinced, check out this [bunny][bunny] with a pancake on its head.
|
||||
|
||||
[bunny]: http://www.thepartyanimal-blog.org/wp-content/uploads/2012/04/pancake_bunny.jpg
|
||||
|
||||
@@ -27,7 +29,7 @@ X is cool, and everyone is using it, so why mess with a good thing?
|
||||
|
||||
**Light.** Only the essentials are included, with the "six" Python 3 compatibility module being the only dependency outside the standard library. We work to keep the code lean and mean, making Falcon easier to test, optimize, and deploy.
|
||||
|
||||
**Cloudy.** Falcon uses the web-friendly Python language and speaks WSGI. Built-in diagnostics facilitate monitoring and debugging of production systems.
|
||||
**Flexible.** Falcon uses the web-friendly Python language and speaks WSGI. Built-in diagnostics facilitate monitoring and debugging of production systems.
|
||||
|
||||
### Install ###
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import random
|
||||
import argparse
|
||||
from timeit import repeat
|
||||
import timeit
|
||||
|
||||
from create import *
|
||||
|
||||
@@ -16,9 +17,9 @@ def avg(array):
|
||||
return sum(array) / len(array)
|
||||
|
||||
|
||||
def bench(name, iterations=10000):
|
||||
def bench(name, iterations=10000, repeat=5):
|
||||
func = create_bench(name)
|
||||
results = repeat(func, number=iterations)
|
||||
results = timeit.repeat(func, number=iterations, repeat=repeat)
|
||||
|
||||
sec_per_req = avg(results) / iterations
|
||||
|
||||
@@ -50,7 +51,8 @@ if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Falcon benchmark runner")
|
||||
parser.add_argument('-b', '--benchmark', type=str, action='append',
|
||||
choices=frameworks, dest='frameworks')
|
||||
parser.add_argument('-i', '--iterations', type=int, default=10000)
|
||||
parser.add_argument('-i', '--iterations', type=int, default=100000)
|
||||
parser.add_argument('-r', '--repetitions', type=int, default=10)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.frameworks:
|
||||
@@ -63,7 +65,8 @@ if __name__ == '__main__':
|
||||
|
||||
sys.stdout.write('\nBenchmarking')
|
||||
sys.stdout.flush()
|
||||
results = [bench(framework, args.iterations) for framework in frameworks]
|
||||
results = [bench(framework, args.iterations, args.repetitions)
|
||||
for framework in frameworks]
|
||||
print('done.\n')
|
||||
|
||||
results = sorted(results, key=lambda r: r[1])
|
||||
@@ -72,9 +75,9 @@ if __name__ == '__main__':
|
||||
for i, (name, sec_per_req) in enumerate(results):
|
||||
req_per_sec = 1 / sec_per_req
|
||||
ms_per_req = sec_per_req * 1000
|
||||
factor = int(baseline / sec_per_req)
|
||||
factor = int(baseline / sec_per_req + 0.1)
|
||||
|
||||
print('{3}. {0:.<15s}{1:.>06,.0f} req/sec or {2:0.3f} ms/req ({4}x)'.
|
||||
format(name, req_per_sec, ms_per_req, i + 1, factor))
|
||||
print('{3}. {0:.<15s}{1:.>06,.0f} req/sec or {2:0.1f} μs/req ({4}x)'.
|
||||
format(name, req_per_sec, ms_per_req * 1000, i + 1, factor))
|
||||
|
||||
print('')
|
||||
|
||||
@@ -22,17 +22,18 @@ del sys.path[-1]
|
||||
|
||||
def create_falcon(body, headers):
|
||||
path = '/hello/{account_id}/test'
|
||||
falcon_app = falcon.API()
|
||||
falcon_app = falcon.API('text/plain')
|
||||
|
||||
class HelloResource:
|
||||
def on_get(self, req, resp, account_id):
|
||||
limit = req.get_param('limit', '10')
|
||||
limit = req.get_param('limit', '10') # NOQA
|
||||
if six.PY3:
|
||||
resp.body = body
|
||||
else:
|
||||
resp.data = body
|
||||
|
||||
resp.set_header('Content-Type', 'text/plain')
|
||||
resp.vary = ['accept-encoding', 'x-auth-token']
|
||||
resp.content_range = (0, 499, 10240)
|
||||
resp.set_headers(headers)
|
||||
|
||||
falcon_app.add_route(path, HelloResource())
|
||||
@@ -47,7 +48,7 @@ def create_wheezy(body, headers):
|
||||
try:
|
||||
limit = query['limit']
|
||||
except KeyError:
|
||||
limit = '10'
|
||||
limit = '10' # NOQA
|
||||
|
||||
response = wheezy.HTTPResponse(content_type='text/plain')
|
||||
response.write_bytes(body)
|
||||
@@ -97,7 +98,7 @@ def create_bottle(body, headers):
|
||||
|
||||
@bottle.route(path)
|
||||
def hello(account_id):
|
||||
limit = bottle.request.query.limit or '10'
|
||||
limit = bottle.request.query.limit or '10' # NOQA
|
||||
return bottle.Response(body, headers=headers)
|
||||
|
||||
return bottle.default_app()
|
||||
@@ -109,7 +110,7 @@ def create_werkzeug(body, headers):
|
||||
|
||||
@werkzeug.Request.application
|
||||
def hello(request):
|
||||
limit = request.args.get('limit', '10')
|
||||
limit = request.args.get('limit', '10') # NOQA
|
||||
adapter = url_map.bind_to_environ(request.environ)
|
||||
endpoint, values = adapter.match()
|
||||
return werkzeug.Response(body, headers=headers,
|
||||
|
||||
@@ -22,3 +22,4 @@ from falcon.api import API, DEFAULT_MEDIA_TYPE
|
||||
from falcon.status_codes import *
|
||||
from falcon.exceptions import *
|
||||
from falcon.http_error import HTTPError
|
||||
from falcon.util import dt_to_http
|
||||
|
||||
@@ -16,6 +16,8 @@ limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
import falcon
|
||||
|
||||
CONTENT_TYPE_NAMES = set(['Content-Type', 'content-type', 'CONTENT-TYPE'])
|
||||
|
||||
|
||||
@@ -25,18 +27,48 @@ class Response(object):
|
||||
|
||||
Attributes:
|
||||
status: HTTP status code, such as "200 OK" (see also falcon.HTTP_*)
|
||||
|
||||
body: String representing response content. If Unicode, Falcon will
|
||||
encode as UTF-8 in the response. If data is already a byte string,
|
||||
use the data attribute instead (it's faster).
|
||||
data: Byte string representing response content.
|
||||
stream: Iterable stream-like object, representing response content.
|
||||
stream_len: Expected length of stream (e.g., file size).
|
||||
|
||||
content_type: Value for the Content-Type header
|
||||
etag: Value for the ETag header
|
||||
cache_control: An array of cache directives (see http://goo.gl/fILS5
|
||||
and http://goo.gl/sM9Xx for a good description.) The array will be
|
||||
joined with ', ' to produce the value for the Cache-Control
|
||||
header.
|
||||
last_modified: A datetime (UTC) instance to use as the Last-Modified
|
||||
header. Falcon will format the datetime as an HTTP date. See
|
||||
also: http://goo.gl/R7So4
|
||||
retry_after: Number of seconds to use as the value for the Retry-After
|
||||
header. Note that the HTTP-date option is not supported. See
|
||||
also: http://goo.gl/DIrWr
|
||||
vary: Value to use for the Vary header. From Wikipedia: "Tells
|
||||
downstream proxies how to match future request headers to decide
|
||||
whether the cached response can be used rather than requesting a
|
||||
fresh one from the origin server." See also: http://goo.gl/NGHdL
|
||||
|
||||
Assumed to be an array of values. For a single asterisk or field
|
||||
value, simply pass a single-element array.
|
||||
location: Value for the Location header. Note that relative URIs are
|
||||
OK per http://goo.gl/DbVqR
|
||||
content_location: Value for the Content-Location header. See
|
||||
also: http://goo.gl/1slsA
|
||||
content_range: A tuple to use in constructing a value for the
|
||||
Content-Range header. The tuple has the form (start, end, length),
|
||||
where start and end is the inclusive byte range, and length is the
|
||||
total number of bytes, or '*' if unknown.
|
||||
|
||||
Note: You only need to use the alternate form, "bytes */1234", for
|
||||
responses that use the status "416 Range Not Satisfiable". In this
|
||||
case, raising falcon.HTTPRangeNotSatisfiable will do the right
|
||||
thing.
|
||||
|
||||
See also: http://goo.gl/Iglhp
|
||||
|
||||
|
||||
"""
|
||||
@@ -44,13 +76,19 @@ class Response(object):
|
||||
__slots__ = (
|
||||
'body',
|
||||
'cache_control',
|
||||
'content_location',
|
||||
'content_range',
|
||||
'content_type',
|
||||
'data',
|
||||
'etag',
|
||||
'_headers',
|
||||
'last_modified',
|
||||
'location',
|
||||
'retry_after',
|
||||
'status',
|
||||
'stream',
|
||||
'stream_len'
|
||||
'stream_len',
|
||||
'vary'
|
||||
)
|
||||
|
||||
def __init__(self, default_media_type):
|
||||
@@ -72,6 +110,12 @@ class Response(object):
|
||||
self.content_type = None
|
||||
self.etag = None
|
||||
self.cache_control = None
|
||||
self.last_modified = None
|
||||
self.retry_after = None
|
||||
self.vary = None
|
||||
self.location = None
|
||||
self.content_location = None
|
||||
self.content_range = None
|
||||
|
||||
def set_header(self, name, value):
|
||||
"""Set a header for this response to a given value.
|
||||
@@ -108,6 +152,40 @@ class Response(object):
|
||||
|
||||
self._headers.extend(headers.items())
|
||||
|
||||
def _append_attribute_headers(self, headers): # NOQA
|
||||
if self.etag is not None:
|
||||
headers.append(('ETag', self.etag))
|
||||
|
||||
if self.cache_control is not None:
|
||||
headers.append(('Cache-Control', ', '.join(self.cache_control)))
|
||||
|
||||
if self.last_modified is not None:
|
||||
headers.append(('Last-Modified',
|
||||
falcon.dt_to_http(self.last_modified)))
|
||||
|
||||
if self.retry_after is not None:
|
||||
headers.append(('Retry-After', str(self.retry_after)))
|
||||
|
||||
if self.vary is not None:
|
||||
headers.append(('Vary', ', '.join(self.vary)))
|
||||
|
||||
if self.location is not None:
|
||||
headers.append(('Location', self.location))
|
||||
|
||||
if self.content_location is not None:
|
||||
headers.append(('Content-Location', self.content_location))
|
||||
|
||||
content_range = self.content_range
|
||||
if content_range is not None:
|
||||
# PERF: Concatenation is faster than % string formatting as well
|
||||
# as ''.join() in this case.
|
||||
formatted_range = ('bytes ' +
|
||||
str(content_range[0]) + '-' +
|
||||
str(content_range[1]) + '/' +
|
||||
str(content_range[2]))
|
||||
|
||||
headers.append(('Content-Range', formatted_range))
|
||||
|
||||
def _wsgi_headers(self, set_content_type):
|
||||
"""Convert headers into the format expected by WSGI servers
|
||||
|
||||
@@ -122,10 +200,6 @@ class Response(object):
|
||||
elif self.content_type is not None:
|
||||
headers.append(('Content-Type', self.content_type))
|
||||
|
||||
if self.etag is not None:
|
||||
headers.append(('ETag', self.etag))
|
||||
|
||||
if self.cache_control is not None:
|
||||
headers.append(('Cache-Control', ', '.join(self.cache_control)))
|
||||
self._append_attribute_headers(headers)
|
||||
|
||||
return headers
|
||||
|
||||
33
falcon/util.py
Normal file
33
falcon/util.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Defines Falcon utility functions
|
||||
|
||||
Copyright 2013 by Rackspace Hosting, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def dt_to_http(dt):
|
||||
"""Converts a datetime instance to an HTTP date string.
|
||||
|
||||
Args:
|
||||
dt: A datetime object, assumed to be UTC
|
||||
|
||||
Returns:
|
||||
An HTTP date string, e.g., "Tue, 15 Nov 1994 12:45:26 GMT". See
|
||||
also: http://goo.gl/R7So4
|
||||
|
||||
"""
|
||||
|
||||
# Tue, 15 Nov 1994 12:45:26 GMT
|
||||
return dt.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
from testtools.matchers import Contains, Not
|
||||
|
||||
import falcon
|
||||
@@ -34,6 +36,12 @@ class DefaultContentTypeResource:
|
||||
|
||||
class HeaderHelpersResource:
|
||||
|
||||
def __init__(self, last_modified=None):
|
||||
if last_modified is not None:
|
||||
self.last_modified = last_modified
|
||||
else:
|
||||
self.last_modified = datetime.utcnow()
|
||||
|
||||
def on_get(self, req, resp):
|
||||
resp.body = "{}"
|
||||
resp.content_type = 'x-falcon/peregrine'
|
||||
@@ -43,15 +51,15 @@ class HeaderHelpersResource:
|
||||
]
|
||||
|
||||
resp.etag = 'fa0d1a60ef6616bb28038515c8ea4cb2'
|
||||
# resp.set_last_modified() # http://goo.gl/M9Fs9
|
||||
# resp.set_retry_after() # http://goo.gl/DIrWr
|
||||
# resp.set_vary() # http://goo.gl/wyI7d
|
||||
resp.last_modified = self.last_modified
|
||||
resp.retry_after = 3601
|
||||
|
||||
# # Relative URI's are OK per http://goo.gl/DbVqR
|
||||
# resp.set_location('/things/87')
|
||||
# Relative URI's are OK per http://goo.gl/DbVqR
|
||||
resp.location = '/things/87'
|
||||
resp.content_location = '/things/78'
|
||||
|
||||
# bytes 0-499/10240
|
||||
#resp.set_content_range(0, 499, 10 * 1024)
|
||||
resp.content_range = (0, 499, 10 * 1024)
|
||||
|
||||
def on_head(self, req, resp):
|
||||
# Alias of set_media_type
|
||||
@@ -60,6 +68,16 @@ class HeaderHelpersResource:
|
||||
resp.cache_control = ['no-store']
|
||||
|
||||
|
||||
class VaryHeaderResource:
|
||||
|
||||
def __init__(self, vary):
|
||||
self.vary = vary
|
||||
|
||||
def on_get(self, req, resp):
|
||||
resp.body = "{}"
|
||||
resp.vary = self.vary
|
||||
|
||||
|
||||
class TestHeaders(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
@@ -183,7 +201,8 @@ class TestHeaders(helpers.TestSuite):
|
||||
self.assertIn(('Content-Type', content_type), self.srmock.headers)
|
||||
|
||||
def test_response_header_helpers_on_get(self):
|
||||
self.resource = HeaderHelpersResource()
|
||||
last_modified = datetime(2013, 1, 1, 10, 30, 30)
|
||||
self.resource = HeaderHelpersResource(last_modified)
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
self._simulate_request(self.test_route)
|
||||
|
||||
@@ -199,6 +218,16 @@ class TestHeaders(helpers.TestSuite):
|
||||
etag = 'fa0d1a60ef6616bb28038515c8ea4cb2'
|
||||
self.assertIn(('ETag', etag), self.srmock.headers)
|
||||
|
||||
last_modified_http_date = 'Tue, 01 Jan 2013 10:30:30 GMT'
|
||||
self.assertIn(('Last-Modified', last_modified_http_date),
|
||||
self.srmock.headers)
|
||||
|
||||
self.assertIn(('Retry-After', '3601'), self.srmock.headers)
|
||||
self.assertIn(('Location', '/things/87'), self.srmock.headers)
|
||||
self.assertIn(('Content-Location', '/things/78'), self.srmock.headers)
|
||||
self.assertIn(('Content-Range', 'bytes 0-499/10240'),
|
||||
self.srmock.headers)
|
||||
|
||||
def test_response_header_helpers_on_head(self):
|
||||
self.resource = HeaderHelpersResource()
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
@@ -209,6 +238,28 @@ class TestHeaders(helpers.TestSuite):
|
||||
|
||||
self.assertIn(('Cache-Control', 'no-store'), self.srmock.headers)
|
||||
|
||||
def test_vary_star(self):
|
||||
self.resource = VaryHeaderResource(['*'])
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
self._simulate_request(self.test_route)
|
||||
|
||||
self.assertIn(('Vary', '*'), self.srmock.headers)
|
||||
|
||||
def test_vary_header(self):
|
||||
self.resource = VaryHeaderResource(['accept-encoding'])
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
self._simulate_request(self.test_route)
|
||||
|
||||
self.assertIn(('Vary', 'accept-encoding'), self.srmock.headers)
|
||||
|
||||
def test_vary_headers(self):
|
||||
self.resource = VaryHeaderResource(['accept-encoding', 'x-auth-token'])
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
self._simulate_request(self.test_route)
|
||||
|
||||
vary = 'accept-encoding, x-auth-token'
|
||||
self.assertIn(('Vary', vary), self.srmock.headers)
|
||||
|
||||
def test_no_content_type(self):
|
||||
self.resource = DefaultContentTypeResource()
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
|
||||
Reference in New Issue
Block a user