Files
deb-python-falcon/falcon/api_helpers.py
kgriffs 8778103541 doc(reference): Improved and annotated Request docstrings
The Request docstrings were modified so that now attributes and properties
are both documented in the class docstring, and class members were
reordered to be grouped more logically so that browsing the source as
well as reading the docs is easier.

Also, None was annotated across multiple files, and some minor tweaks to
introductory prose were completed.
2014-04-08 18:11:19 -05:00

273 lines
8.2 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
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):
"""Converts resp content into an iterable as required by PEP 333
Args:
resp: Instance of falcon.Response
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
* 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:
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 '/').
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