Files
deb-python-falcon/falcon/api_helpers.py
kgriffs 49604c1d69 doc: Add FAQ
This patch introduces a new FAQ, and adds some FAQ-like info inline
to doctrings where appropriate. NOTES.md is no longer necessary now
that we have its content properly documented, and so it was removed.
2014-04-14 20:08:04 -04:00

297 lines
9.4 KiB
Python

# 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.
import re
from functools import wraps
from falcon import responders, HTTP_METHODS
import falcon.status_codes as status
STREAM_BLOCK_SIZE = 8 * 1024 # 8 KiB
IGNORE_BODY_STATUS_CODES = set([
status.HTTP_100,
status.HTTP_101,
status.HTTP_204,
status.HTTP_416,
status.HTTP_304
])
def prepare_global_hooks(hooks):
if hooks is not None:
if not isinstance(hooks, list):
hooks = [hooks]
for action in hooks:
if not callable(action):
raise TypeError('One or more hooks are not callable')
return hooks
def should_ignore_body(status, method):
"""Return True if the status or method indicates no body, per RFC 2616
Args:
status: An HTTP status line, e.g., "204 No Content"
Returns:
True if method is HEAD, or the status is 1xx, 204, or 304; returns
False otherwise.
"""
return (method == 'HEAD' or status in IGNORE_BODY_STATUS_CODES)
def set_content_length(resp):
"""Set Content-Length when given a fully-buffered body or stream length
Pre:
Either resp.body or resp.stream is set
Post:
resp contains a "Content-Length" header unless a stream is given, but
resp.stream_len is not set (in which case, the length cannot be
derived reliably).
Args:
resp: The response object on which to set the content length.
"""
content_length = 0
if resp.body_encoded is not None:
# Since body is assumed to be a byte string (str in Python 2, bytes in
# Python 3), figure out the length using standard functions.
content_length = len(resp.body_encoded)
elif resp.data is not None:
content_length = len(resp.data)
elif resp.stream is not None:
if resp.stream_len is not None:
# Total stream length is known in advance (e.g., streaming a file)
content_length = resp.stream_len
else:
# Stream given, but length is unknown (dynamically-generated body)
# ...do not set the header.
return -1
resp.set_header('Content-Length', str(content_length))
return content_length
def get_body(resp, wsgi_file_wrapper=None):
"""Converts resp content into an iterable as required by PEP 333
Args:
resp: Instance of falcon.Response
wsgi_file_wrapper: Reference to wsgi.file_wrapper from the
WSGI environ dict, if provided by the WSGI server. Used
when resp.stream is a file-like object (default None).
Returns:
* If resp.body is not *None*, returns [resp.body], encoded as UTF-8 if
it is a Unicode string. Bytestrings are returned as-is.
* If resp.data is not *None*, returns [resp.data]
* If resp.stream is not *None*, returns resp.stream
iterable using wsgi.file_wrapper, if possible.
* Otherwise, returns []
"""
body = resp.body_encoded
if body is not None:
return [body]
elif resp.data is not None:
return [resp.data]
elif resp.stream is not None:
stream = resp.stream
# NOTE(kgriffs): Heuristic to quickly check if
# stream is file-like. Not perfect, but should be
# good enough until proven otherwise.
if hasattr(stream, 'read'):
if wsgi_file_wrapper is not None:
# TODO(kgriffs): Make block size configurable at the
# global level, pending experimentation to see how
# useful that would be.
#
# See also the discussion on the PR: http://goo.gl/XGrtDz
return wsgi_file_wrapper(stream, STREAM_BLOCK_SIZE)
else:
return iter(lambda: stream.read(STREAM_BLOCK_SIZE),
b'')
return resp.stream
return []
def compile_uri_template(template):
"""Compile the given URI template string into a pattern matcher.
Currently only recognizes Level 1 URI templates, and only for the path
portion of the URI.
See also: http://tools.ietf.org/html/rfc6570
Args:
template: A Level 1 URI template. Method responders must accept, as
arguments, all fields specified in the template (default '/').
Note that field names are restricted to ASCII a-z, A-Z, and
the underscore '_'.
Returns:
(template_field_names, template_regex)
"""
if not isinstance(template, str):
raise TypeError('uri_template is not a string')
if not template.startswith('/'):
raise ValueError("uri_template must start with '/'")
if '//' in template:
raise ValueError("uri_template may not contain '//'")
if template != '/' and template.endswith('/'):
template = template[:-1]
expression_pattern = r'{([a-zA-Z][a-zA-Z_]*)}'
# Get a list of field names
fields = set(re.findall(expression_pattern, template))
# Convert Level 1 var patterns to equivalent named regex groups
escaped = re.sub(r'[\.\(\)\[\]\?\*\+\^\|]', r'\\\g<0>', template)
pattern = re.sub(expression_pattern, r'(?P<\1>[^/]+)', escaped)
pattern = r'\A' + pattern + r'\Z'
return fields, re.compile(pattern, re.IGNORECASE)
def create_http_method_map(resource, uri_fields, before, after):
"""Maps HTTP methods (such as GET and POST) to methods of resource object
Args:
resource: An object with "responder" methods, starting with on_*, that
correspond to each method the resource supports. For example, if a
resource supports GET and POST, it should define
on_get(self, req, resp) and on_post(self,req,resp).
uri_fields: A set of field names from the route's URI template that
a responder must support in order to avoid "method not allowed".
before: An action hook or list of hooks to be called before each
on_* responder defined by the resource.
after: An action hook or list of hooks to be called after each on_*
responder defined by the resource.
Returns:
A tuple containing a dict mapping HTTP methods to responders, and
the method-not-allowed responder.
"""
method_map = {}
for method in HTTP_METHODS:
try:
responder = getattr(resource, 'on_' + method.lower())
except AttributeError:
# resource does not implement this method
pass
else:
# Usually expect a method, but any callable will do
if callable(responder):
responder = _wrap_with_hooks(before, after, responder)
method_map[method] = responder
# Attach a resource for unsupported HTTP methods
allowed_methods = sorted(list(method_map.keys()))
# NOTE(sebasmagri): We want the OPTIONS and 405 (Not Allowed) methods
# responders to be wrapped on global hooks
if 'OPTIONS' not in method_map:
# OPTIONS itself is intentionally excluded from the Allow header
responder = responders.create_default_options(
allowed_methods)
method_map['OPTIONS'] = _wrap_with_hooks(before, after, responder)
allowed_methods.append('OPTIONS')
na_responder = responders.create_method_not_allowed(allowed_methods)
for method in HTTP_METHODS:
if method not in allowed_methods:
method_map[method] = _wrap_with_hooks(before, after, na_responder)
return method_map
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
def _wrap_with_hooks(before, after, responder):
if after is not None:
for action in after:
responder = _wrap_with_after(action, responder)
if before is not None:
# Wrap in reversed order to achieve natural (first...last)
# execution order.
for action in reversed(before):
responder = _wrap_with_before(action, responder)
return responder
def _wrap_with_before(action, responder):
"""Execute the given action function before a bound responder.
Args:
action: A function with a similar signature to a resource responder
method, taking (req, resp, params).
responder: The bound responder to wrap.
"""
@wraps(responder)
def do_before(req, resp, **kwargs):
action(req, resp, kwargs)
responder(req, resp, **kwargs)
return do_before
def _wrap_with_after(action, responder):
"""Execute the given action function after a bound responder.
Args:
action: A function with a signature similar to a resource responder
method, taking (req, resp).
responder: The bound responder to wrap.
"""
@wraps(responder)
def do_after(req, resp, **kwargs):
responder(req, resp, **kwargs)
action(req, resp)
return do_after