From 749247699cd0f817092b17d8d8432dc811094b22 Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Tue, 18 Nov 2014 10:38:57 -0600 Subject: [PATCH 1/8] doc(hooks): Add resource param to examples Add the new resource param to the examples in the hook documentation. --- doc/api/hooks.rst | 2 +- falcon/hooks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/hooks.rst b/doc/api/hooks.rst index 017f60133..38d56ac08 100644 --- a/doc/api/hooks.rst +++ b/doc/api/hooks.rst @@ -11,7 +11,7 @@ For example, suppose you had a hook like this: .. code:: python - def validate_image_type(req, resp, params): + def validate_image_type(req, resp, resource, params): if req.content_type not in ALLOWED_IMAGE_TYPES: msg = 'Image type not allowed. Must be PNG, JPEG, or GIF' raise falcon.HTTPBadRequest('Bad request', msg) diff --git a/falcon/hooks.py b/falcon/hooks.py index 11e577ed2..d1bfcc9f1 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -33,7 +33,7 @@ def before(action): Note: Hooks may inject extra params as needed. For example:: - def do_something(req, resp, params): + def do_something(req, resp, resource, params): try: params['id'] = int(params['id']) except ValueError: From ffd505c8f60d5887cfe266cc4b9df6d1233be892 Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Thu, 20 Nov 2014 11:04:24 -0600 Subject: [PATCH 2/8] doc(API): Clarify that middleware expects instantiated class objects Add a note to the middleware param documentation to ensure people don't mistakenly pass class types in lieu of instances of said classes. --- falcon/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/falcon/api.py b/falcon/api.py index 34a7c1ad5..1f9a218f6 100644 --- a/falcon/api.py +++ b/falcon/api.py @@ -41,8 +41,9 @@ class API(object): after (callable, optional): A global action hook (or list of hooks) to call after each on_* responder, for all resources. Similar to the ``after`` decorator, but applies to the entire API. - middleware(object or list, optional): One or more objects that - implement the following middleware component interface:: + middleware(object or list, optional): One or more objects ( + instantiated classes) that implement the following middleware + component interface:: class ExampleComponent(object): def process_request(self, req, resp, params): From 0a75607fd34720d5c37672a6779f2d1aea476634 Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Thu, 20 Nov 2014 11:53:29 -0600 Subject: [PATCH 3/8] doc(README): Add note about webob and code optimization to features list Better express three of Falcon's most attractive features: performance, clean code, and no dependency on WebOb. --- README.md | 5 +++-- README.rst | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5b65e2c18..f6f1e817f 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,15 @@ Falcon is a [high-performance Python framework][home] for building cloud APIs. I ### Features ### +* Highly-optimized, extensible code base * Intuitive routing via URI templates and resource classes * Easy access to headers and bodies through request and response classes +* Does not use WebOb (some of us do indeed consider this a feature) * Idiomatic HTTP error responses via a handy exception base class * DRY request processing using global, resource, and method hooks * Snappy unit testing through WSGI helpers and mocks -* 20% speed boost when Cython is available * Python 2.6, Python 2.7, PyPy and Python 3.3/3.4 support -* Speed, speed, and more speed! +* 20% speed boost when Cython is available ### Install ### diff --git a/README.rst b/README.rst index b24085551..ee3d5f626 100644 --- a/README.rst +++ b/README.rst @@ -41,15 +41,16 @@ mix-and-match what you need. Features ~~~~~~~~ +- Highly-optimized, extensible code base - Intuitive routing via URI templates and resource classes - Easy access to headers and bodies through request and response classes +- Does not use WebOb (some of us do indeed consider this a feature) - Idiomatic HTTP error responses via a handy exception base class - DRY request processing using global, resource, and method hooks - Snappy unit testing through WSGI helpers and mocks -- 20% speed boost when Cython is available - Python 2.6, Python 2.7, PyPy and Python 3.3/3.4 support -- Speed, speed, and more speed! +- 20% speed boost when Cython is available Install ~~~~~~~ From 2dcdc29e010b8651970819014bb90c6a011277ac Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Tue, 25 Nov 2014 15:25:45 -0600 Subject: [PATCH 4/8] doc(Installation): Add note about installing prelease versions In order to reduce confusion, add a note about how to install the Falcon 0.2 beta and release candidates (as they become available). As part of this change, reconcile some of the differences between the installation instructions in the README and on Read the Docs. --- README.md | 11 ++++++++++- README.rst | 12 +++++++++++- doc/user/install.rst | 12 +++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f6f1e817f..5190d2e8c 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,17 @@ Falcon is a [high-performance Python framework][home] for building cloud APIs. I ### Install ### +> This documentation targets the upcoming 0.2 release of Falcon, +> currently in beta and available on PyPI. You will need to use the +> ``--pre`` flag with pip in order to install the Falcon 0.2 betas +> and release candidates. + +If available, Falcon will compile itself with Cython for an extra +speed boost. The following will make sure Cython is installed first, and +that you always have the latest and greatest. + ```bash -$ pip install cython falcon +$ pip install --upgrade cython falcon ``` **Installing on OS X Mavericks with Xcode 5.1** diff --git a/README.rst b/README.rst index ee3d5f626..b7aeda844 100644 --- a/README.rst +++ b/README.rst @@ -54,10 +54,20 @@ Features Install ~~~~~~~ +.. note:: + + This documentation targets the upcoming 0.2 release of Falcon, + currently in beta and available on PyPI. You will need to use the + ``--pre`` flag with pip in order to install the Falcon 0.2 betas + and release candidates. + +If available, Falcon will compile itself with Cython for an extra +speed boost. The following will make sure Cython is installed first, and +that you always have the latest and greatest. .. code:: bash - $ pip install cython falcon + $ pip install --upgrade cython falcon **Installing on OS X Mavericks with Xcode 5.1** diff --git a/doc/user/install.rst b/doc/user/install.rst index c124a9044..5632569c1 100644 --- a/doc/user/install.rst +++ b/doc/user/install.rst @@ -7,13 +7,20 @@ Install from PyPI ----------------- Falcon is super easy to install with pip. If you don't have pip yet, -please run—don't walk—on over to the +please run—don't walk—to the `pip website `_ and get that happy little tool installed before you do anything else. +.. note:: + + This documentation targets the upcoming 0.2 release of Falcon, + currently in beta and available on PyPI. You will need to use the + ``--pre`` flag with pip in order to install the Falcon 0.2 betas + and release candidates. + If available, Falcon will compile itself with Cython for an extra speed boost. The following will make sure Cython is installed first, and -that you always have the latest and greatest +that you always have the latest and greatest. .. code:: bash @@ -25,7 +32,6 @@ If you are on PyPy, you won't need Cython, of course: $ pip install --upgrade falcon - Installing Cython on OS X ------------------------- From 81c9f2786b9b176b2ced3c2fa2f74df80eb721db Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Mon, 1 Dec 2014 13:19:38 -0600 Subject: [PATCH 5/8] doc(Response): status attribute must be set to a complete HTTP status line Mitigate confusion around resp.status that may cause some developers to assign just a code to it, rather that the entire status line. See also issue #266 --- falcon/response.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/falcon/response.py b/falcon/response.py index bf5de25b3..23e010d1f 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -25,7 +25,13 @@ class Response(object): `Response` is not meant to be instantiated directly by responders. Attributes: - status (str): HTTP status line, such as "200 OK" + status (str): HTTP status line (e.g., '200 OK'). Falcon requires the + full status line, not just the code (e.g., 200). This design + makes the framework more efficient because it does not have to + do any kind of conversion or lookup when composing the WSGI + response. + + If not set explicitly, the status defaults to '200 OK'. Note: Falcon provides a number of constants for common status From 6ff79451c22250bc637216451c651d9451bb0ded Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Wed, 24 Dec 2014 16:09:30 -0600 Subject: [PATCH 6/8] doc(reference): Misc. tweaks to docstrings Audit docstrings and touch up any that are unclear or out of date. Fix minor typos as they are discovered. Partially implements #296 --- falcon/api.py | 127 +++++++++++++++++++++++++++---------------- falcon/hooks.py | 16 +++--- falcon/http_error.py | 20 +++---- falcon/request.py | 60 ++++++++++---------- falcon/response.py | 28 ++++++---- falcon/util/misc.py | 2 +- 6 files changed, 149 insertions(+), 104 deletions(-) diff --git a/falcon/api.py b/falcon/api.py index 1f9a218f6..f00a63afb 100644 --- a/falcon/api.py +++ b/falcon/api.py @@ -27,26 +27,22 @@ class API(object): """This class is the main entry point into a Falcon-based app. - Each API instance provides a callable WSGI interface and a simple routing - engine based on URI Templates (RFC 6570). + Each API instance provides a callable WSGI interface and a routing engine. + + Note: + Global hooks (configured using the `before` and `after` kwargs) are + deprecated in favor of middleware, and may be removed in a future + version of the framework. Args: media_type (str, optional): Default media type to use as the value for the Content-Type header on responses. (default 'application/json') - before (callable, optional): A global action hook (or list of hooks) - to call before each on_* responder, for all resources. Similar to - the ``falcon.before`` decorator, but applies to the entire API. - When more than one hook is given, they will be executed - in natural order (starting with the first in the list). - after (callable, optional): A global action hook (or list of hooks) - to call after each on_* responder, for all resources. Similar to - the ``after`` decorator, but applies to the entire API. middleware(object or list, optional): One or more objects ( instantiated classes) that implement the following middleware component interface:: class ExampleComponent(object): - def process_request(self, req, resp, params): + def process_request(self, req, resp): \"""Process the request before routing it. Args: @@ -60,6 +56,11 @@ def process_response(self, req, resp) \"""Post-processing of the response (after routing). \""" + Middleware components execute both before and after the framework + routes the request, or calls any hooks. For example, if a + component modifies ``req.uri`` in its *process_request* method, + the framework will use the modified value to route the request. + Each component's *process_request* and *process_response* methods are executed hierarchically, as a stack. For example, if a list of middleware objects are passed as ``[mob1, mob2, mob3]``, the order @@ -111,11 +112,12 @@ def process_response(self, req, resp) the framework will execute any remaining middleware on the stack. - request_type (Request, optional): Request-alike class to use instead - of Falcon's default class. Useful if you wish to extend - ``falcon.request.Request`` with a custom ``context_type``. + request_type (Request, optional): Request-like class to use instead + of Falcon's default class. Among other things, this feature + affords inheriting from ``falcon.request.Request`` in order + to override the ``context_type`` class variable. (default falcon.request.Request) - response_type (Response, optional): Response-alike class to use + response_type (Response, optional): Response-like class to use instead of Falcon's default class. (default falcon.response.Response) @@ -264,7 +266,7 @@ def __call__(self, env, start_response): return body def add_route(self, uri_template, resource): - """Associates a URI path with a resource. + """Associates a templatized URI path with a resource. A resource is an instance of a class that defines various on_* "responder" methods, one for each HTTP method the resource @@ -278,26 +280,27 @@ def add_route(self, uri_template, resource): def on_post(self, req, resp): pass - In addition, if the route's uri template contains field + In addition, if the route's template contains field expressions, any responder that desires to receive requests for that route must accept arguments named after the respective - field names defined in the template. For example, given the - following uri template:: + field names defined in the template. A field expression consists + of a bracketed field name. + + For example, given the following template:: - /das/{thing} + /user/{name} - A PUT request to "/das/code" would be routed to:: + A PUT request to "/user/kgriffs" would be routed to:: - def on_put(self, req, resp, thing): + def on_put(self, req, resp, name): pass Args: - uri_template (str): Relative URI template. Currently only Level 1 - templates are supported. See also RFC 6570. Care must be + uri_template (str): A templatized URI. Care must be taken to ensure the template does not mask any sink - patterns (see also ``add_sink``). - resource (instance): Object which represents an HTTP/REST - "resource". Falcon will pass "GET" requests to on_get, + patterns, if any are registered (see also ``add_sink``). + resource (instance): Object which represents a REST + resource. Falcon will pass "GET" requests to on_get, "PUT" requests to on_put, etc. If any HTTP methods are not supported by your resource, simply don't define the corresponding request handlers, and Falcon will do the right @@ -314,11 +317,16 @@ def on_put(self, req, resp, thing): self._routes.insert(0, (path_template, method_map, resource)) def add_sink(self, sink, prefix=r'/'): - """Adds a "sink" responder to the API. + """Registers a sink method for the API. If no route matches a request, but the path in the requested URI - matches the specified prefix, Falcon will pass control to the - given sink, regardless of the HTTP method requested. + matches a sink prefix, Falcon will pass control to the + associated sink, regardless of the HTTP method requested. + + Using sinks, you can drain and dynamically handle a large number + of routes, when creating static resources and responders would be + impractical. For example, you might use a sink to create a smart + proxy that forwards requests to one or more backend services. Args: sink (callable): A callable taking the form ``func(req, resp)``. @@ -334,8 +342,9 @@ def add_sink(self, sink, prefix=r'/'): the sink as such. Note: - If the route collides with a route's URI template, the - route will mask the sink (see also ``add_route``). + If the prefix overlaps a registered route template, + the route will take precedence and mask the sink + (see also ``add_route``). """ @@ -349,28 +358,33 @@ def add_sink(self, sink, prefix=r'/'): self._sinks.insert(0, (prefix, sink)) def add_error_handler(self, exception, handler=None): - """Adds a handler for a given exception type. + """Registers a handler for a given exception error type. Args: exception (type): Whenever an error occurs when handling a request - that is an instance of this exception class, the given - handler callable will be used to handle the exception. - handler (callable): A callable taking the form - ``func(ex, req, resp, params)``, called - when there is a matching exception raised when handling a - request. + that is an instance of this exception class, the associated + handler will be called. + handler (callable): A function or callable object taking the form + ``func(ex, req, resp, params)``. - Note: - If not specified, the handler will default to - ``exception.handle``, where ``exception`` is the error - type specified above, and ``handle`` is a static method - (i.e., decorated with @staticmethod) that accepts - the same params just described. + If not specified explicitly, the handler will default to + ``exception.handle``, where ``exception`` is the error + type specified above, and ``handle`` is a static method + (i.e., decorated with @staticmethod) that accepts + the same params just described. For example: + + class CustomException(CustomBaseException): + + @staticmethod + def handle(ex, req, resp, params): + # TODO: Log the error + # Convert to an instance of falcon.HTTPError + raise falcon.HTTPError(falcon.HTTP_792) Note: A handler can either raise an instance of HTTPError - or modify resp manually in order to communicate information - about the issue to the client. + or modify resp manually in order to communicate + information about the issue to the client. """ @@ -395,6 +409,25 @@ def set_error_serializer(self, serializer): supports JSON and XML, but may be overridden by this method to use a custom serializer in order to support other media types. + The ``falcon.HTTPError`` class contains helper methods, such as + `to_json()` and `to_dict()`, that can be used from within + custom serializers. For example: + + def my_serializer(req, exception): + representation = None + + preferred = req.client_prefers(('application/x-yaml', + 'application/json')) + + if preferred is not None: + if preferred == 'application/json': + representation = exception.to_json() + else: + representation = yaml.dump(exception.to_dict(), + encoding=None) + + return (preferred, representation) + Note: If a custom media type is used and the type includes a "+json" or "+xml" suffix, the default serializer will diff --git a/falcon/hooks.py b/falcon/hooks.py index d1bfcc9f1..b7ccce3f0 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -26,9 +26,10 @@ def before(action): Args: action (callable): A function of the form ``func(req, resp, resource, params)``, where `resource` is a - reference to the resource class associated with the request, - and `params` is a dict of URI Template field names, if any, - that will be passed into the resource responder as *kwargs*. + reference to the resource class instance associated with the + request, and `params` is a dict of URI Template field names, + if any, that will be passed into the resource responder as + *kwargs*. Note: Hooks may inject extra params as needed. For example:: @@ -89,7 +90,8 @@ def after(action): Args: action (callable): A function of the form ``func(req, resp, resource)``, where `resource` is a - reference to the resource class associated with the request + reference to the resource class instance associated with the + request """ @@ -155,7 +157,7 @@ def _get_argspec(func): # pragma: no cover def _has_self(spec): - """Checkes whether the given argspec includes a self param. + """Checks whether the given argspec includes a self param. Warning: If a method's spec lists "self", that doesn't necessarily mean @@ -172,7 +174,7 @@ def _wrap_with_after(action, responder, resource=None, is_method=False): Args: action: A function with a signature similar to a resource responder - method, taking (req, resp). + method, taking (req, resp, resource). responder: The responder method to wrap. resource: The resource affected by `action` (default None). If None, `is_method` MUST BE True, so that the resource can be @@ -221,7 +223,7 @@ def _wrap_with_before(action, responder, resource=None, is_method=False): Args: action: A function with a similar signature to a resource responder - method, taking (req, resp, params) + method, taking (req, resp, resource, params) responder: The responder method to wrap resource: The resource affected by `action` (default None). If None, `is_method` MUST BE True, so that the resource can be diff --git a/falcon/http_error.py b/falcon/http_error.py index dd34504b0..251c27d36 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -34,7 +34,7 @@ class HTTPError(Exception): when something goes wrong. Attributes: - status (str): HTTP status line, such as "748 Confounded by Ponies". + status (str): HTTP status line, e.g. '748 Confounded by Ponies'. has_representation (bool): Read-only property that determines whether error details will be serialized when composing the HTTP response. In ``HTTPError`` this property always @@ -64,7 +64,7 @@ class HTTPError(Exception): wide characters. Note: - The Content-Type header, if present, will be overriden. If + The Content-Type header, if present, will be overridden. If you wish to return custom error messages, you can create your own HTTP error class, and install an error handler to convert it into an appropriate HTTP response for the @@ -195,7 +195,7 @@ class NoRepresentation(object): """Mixin for ``HTTPError`` child classes that have no representation. This class can be mixed in when inheriting from ``HTTPError``, in order - to override the `has_representation` property, such that it always + to override the `has_representation` property such that it always returns ``False``. This, in turn, will cause Falcon to return an empty response body to the client. @@ -218,14 +218,14 @@ def has_representation(self): class OptionalRepresentation(object): """Mixin for ``HTTPError`` child classes that may have a representation. - This class can be mixed in when inheriting from ``HTTPError``, in order - to override the `has_representation` property, such that it optionally - returns ``False``. This, in turn, will cause Falcon to return an empty - response body to the client. + This class can be mixed in when inheriting from ``HTTPError`` in order + to override the `has_representation` property, such that it will + return ``False`` when the error instance has no description + (i.e., the `description` kwarg was not set). - You can use this mixin when defining errors that either may optionally have - a body (as dictated by HTTP standards or common practice), or in the - case that a detailed error response may leak information to an attacker. + You can use this mixin when defining errors that do not include + a body in the HTTP response by default, serializing details only when + the web developer provides a description of the error. Note: This mixin class must appear before ``HTTPError`` in the base class diff --git a/falcon/request.py b/falcon/request.py index 58ffb4755..ff05518e1 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -54,7 +54,7 @@ class Request(object): Args: env (dict): A WSGI environment dict passed in from the server. See - also the PEP-3333 spec. + also PEP-3333. options (dict): Set of global options passed from the API handler. Attributes: @@ -78,11 +78,14 @@ class Request(object): context (dict): Dictionary to hold any data about the request which is specific to your app (e.g. session object). Falcon itself will not interact with this attribute after it has been initialized. - context_type (None): Custom callable/type to use for initializing the - ``context`` attribute. To change this value so that ``context`` - is initialized to the type of your choice (e.g. OrderedDict), you - will need to extend this class and pass that new type to the - ``request_type`` argument of ``falcon.API()``. + context_type (class): Class variable that determines the + factory or type to use for initializing the + ``context`` attribute. By default, the framework will + instantiate standard + ``dict`` objects. However, You may override this behavior + by creating a custom child class of ``falcon.Request``, and + then passing that new class to ``falcon.API()`` by way of the + latter's `request_type` parameter. uri (str): The fully-qualified URI for the request. url (str): alias for ``uri``. relative_uri (str): The path + query string portion of the full URI. @@ -94,12 +97,12 @@ class Request(object): missing. auth (str): Value of the Authorization header, or *None* if the header is missing. - client_accepts_json (bool): True if the Accept header includes JSON, - otherwise False. - client_accepts_msgpack (bool): True if the Accept header includes - msgpack, otherwise False. - client_accepts_xml (bool): True if the Accept header includes XML, - otherwise False. + client_accepts_json (bool): True if the Accept header indicates that + the client is willing to receive JSON, otherwise False. + client_accepts_msgpack (bool): True if the Accept header indicates + that the client is willing to receive MessagePack, otherwise False. + client_accepts_xml (bool): True if the Accept header indicates that + the client is willing to receive XML, otherwise False. content_type (str): Value of the Content-Type header, or *None* if the header is missing. content_length (int): Value of the Content-Length header converted @@ -504,7 +507,7 @@ def client_accepts(self, media_type): return False def client_prefers(self, media_types): - """Returns the client's preferred media type given several choices. + """Returns the client's preferred media type, given several choices. Args: media_types (iterable of str): One or more Internet media types @@ -527,7 +530,7 @@ def client_prefers(self, media_types): return (preferred_type if preferred_type else None) def get_header(self, name, required=False): - """Return a header value as a string. + """Return a raw header value as a string. Args: name (str): Header name, case-insensitive (e.g., 'Content-Type') @@ -571,7 +574,7 @@ def get_header(self, name, required=False): raise HTTPMissingParam(name) def get_param(self, name, required=False, store=None): - """Return the value of a query string parameter as a string. + """Return the raw value of a query string parameter as a string. Note: If an HTML form is POSTed to the API using the @@ -581,13 +584,14 @@ def get_param(self, name, required=False, store=None): If a key appears more than once in the form data, one of the values will be returned as a string, but it is undefined which - one. Use .get_param_as_list() to retrieve all the values. + one. Use `req.get_param_as_list()` to retrieve all the values. Note: - If a query parameter is assigned a comma-separated list of - values (e.g., foo=a,b,c) then only one of the values will be + Similar to the way multiple keys in form data is handled, + if a query parameter is assigned a comma-separated list of + values (e.g., foo=a,b,c), only one of those values will be returned, and it is undefined which one. Use - .get_param_as_list() to retrieve all the values. + `req.get_param_as_list()` to retrieve all the values. Args: name (str): Parameter name, case-sensitive (e.g., 'sort') @@ -595,15 +599,14 @@ def get_param(self, name, required=False, store=None): instead of returning gracefully when the parameter is not found (default False) store (dict, optional): A dict-like object in which to place the - value of the param, but only if the param is found. + value of the param, but only if the param is present. Returns: string: The value of the param as a string, or *None* if param is not found and is not required. Raises: - HTTPBadRequest: The param was not found in the request, but was - required. + HTTPBadRequest: A required param is missing from the request. """ @@ -720,9 +723,8 @@ def get_param_as_bool(self, name, required=False, store=None, to a boolean. If the param is not found, returns *None* unless required is True. - Raises - HTTPBadRequest: The param was not found in the request, even though - it was required to be there. + Raises: + HTTPBadRequest: A required param is missing from the request. """ @@ -792,9 +794,9 @@ def get_param_as_list(self, name, ['1', '3'] - Raises - HTTPBadRequest: The param was not found in the request, but was - required. + Raises: + HTTPBadRequest: A required param is missing from the request. + """ params = self._params @@ -839,7 +841,7 @@ def log_error(self, message): # pragma: no cover result out to the WSGI server's error stream (`wsgi.error`). Args: - message (str): A string describing the problem. If a byte-string + message (str): A string describing the problem. If a byte-string, it is simply written out as-is. Unicode strings will be converted to UTF-8. diff --git a/falcon/response.py b/falcon/response.py index 23e010d1f..f9f08e604 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -45,17 +45,24 @@ class Response(object): body_encoded (bytes): Returns a UTF-8 encoded version of `body`. data (bytes): Byte string representing response content. + Use this attribute in lieu of `body` when your content is + already a byte string (``str`` or ``bytes`` in Python 2, or + simply ``bytes`` in Python 3). See also the note below. + Note: - Under Python 2.x, if your content is of type *str*, setting - this rather than body will be most efficient. However, if - your text is of type *unicode*, you will want to use the + Under Python 2.x, if your content is of type *str*, using + the `data` attribute instead of `body` is the most + efficient approach. However, if + your text is of type *unicode*, you will need to use the *body* attribute instead. - Under Python 3.x, the 2.x *str* type can be thought of as - having been replaced with what was once the *unicode* type, - and so you will want to use the `body` attribute to + Under Python 3.x, on the other hand, the 2.x *str* type can + be thought of as + having been replaced by what was once the *unicode* type, + and so you will need to always use the `body` attribute for + strings to ensure Unicode characters are properly encoded in the - response body. + HTTP response. stream: Either a file-like object with a *read()* method that takes an optional size argument and returns a block of bytes, or an @@ -150,11 +157,12 @@ def set_header(self, name, value): self._headers[name.lower()] = value def append_header(self, name, value): - """Set or append a header for this response to a given value. + """Set or append a header for this response. Warning: - Calling this method will append any existing value using comma - separation. Please ensure the header type supports this. + If the header already exists, the new value will be appended + to it, delimited by a comma. Most header specifications support + this format, Cookie and Set-Cookie being the notable exceptions. Args: name (str): Header name to set (case-insensitive). Must be of diff --git a/falcon/util/misc.py b/falcon/util/misc.py index 0be83d7e1..f3598f4a4 100644 --- a/falcon/util/misc.py +++ b/falcon/util/misc.py @@ -145,7 +145,7 @@ def get_bound_method(obj, method_name): method_name: Name of the method to retrieve. Returns: - Bound method, or `None` if the method does not exist on` + Bound method, or `None` if the method does not exist on the object. Raises: From 87236e8c1c40a6c334d31c60fcd169b07863f865 Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Wed, 24 Dec 2014 18:25:47 -0600 Subject: [PATCH 7/8] doc(guide): Update quickstart example code Update the README and quickstart example code to showcase the new middleware feature. Also tweak other parts of the example code, as needed, to be in sync with the latest updates to the framework. Partially implements #296 --- README.md | 170 ++++++++++++++++++++++++--------------- README.rst | 168 ++++++++++++++++++++++++--------------- doc/user/quickstart.rst | 172 ++++++++++++++++++++++++---------------- doc/user/tutorial.rst | 8 +- tests/test_example.py | 168 ++++++++++++++++++++++++--------------- 5 files changed, 424 insertions(+), 262 deletions(-) diff --git a/README.md b/README.md index 5190d2e8c..936afc26a 100644 --- a/README.md +++ b/README.md @@ -169,24 +169,27 @@ $ curl localhost:8000/things Here is a more involved example that demonstrates reading headers and query parameters, handling errors, and working with request and response bodies. ```python - import json import logging import uuid from wsgiref import simple_server import falcon +import requests class StorageEngine(object): + def get_things(self, marker, limit): - return [] + return [{'id': str(uuid.uuid4()), 'color': 'green'}] def add_thing(self, thing): - return {'id': str(uuid.uuid4())} + thing['id'] = str(uuid.uuid4()) + return thing class StorageError(Exception): + @staticmethod def handle(ex, req, resp, params): description = ('Sorry, couldn\'t write your thing to the ' @@ -197,82 +200,110 @@ class StorageError(Exception): description) -class Proxy(object): - def forward(self, req): - return falcon.HTTP_503 - - class SinkAdapter(object): - def __init__(self): - self._proxy = Proxy() + engines = { + 'ddg': 'https://duckduckgo.com', + 'y': 'https://search.yahoo.com/search', + } - def __call__(self, req, resp, **kwargs): - resp.status = self._proxy.forward(req) - self.kwargs = kwargs + def __call__(self, req, resp, engine): + url = self.engines[engine] + params = {'q': req.get_param('q', True)} + result = requests.get(url, params=params) + resp.status = str(result.status_code) + ' ' + result.reason + resp.content_type = result.headers['content-type'] + resp.body = result.text -def token_is_valid(token, user_id): - return True # Suuuuuure it's valid... +class AuthMiddleware(object): -def auth(req, resp, params): - # Alternatively, use Talons or do this in WSGI middleware... - token = req.get_header('X-Auth-Token') + def process_request(self, req, resp): + token = req.get_header('X-Auth-Token') + project = req.get_header('X-Project-ID') - if token is None: - description = ('Please provide an auth token ' - 'as part of the request.') + if token is None: + description = ('Please provide an auth token ' + 'as part of the request.') - raise falcon.HTTPUnauthorized('Auth token required', - description, - href='http://docs.example.com/auth') + raise falcon.HTTPUnauthorized('Auth token required', + description, + href='http://docs.example.com/auth') - if not token_is_valid(token, params['user_id']): - description = ('The provided auth token is not valid. ' - 'Please request a new token and try again.') + if not self._token_is_valid(token, project): + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') - raise falcon.HTTPUnauthorized('Authentication required', - description, - href='http://docs.example.com/auth', - scheme='Token; UUID') + raise falcon.HTTPUnauthorized('Authentication required', + description, + href='http://docs.example.com/auth', + scheme='Token; UUID') + def _token_is_valid(self, token, project): + return True # Suuuuuure it's valid... -def check_media_type(req, resp, params): - if not req.client_accepts_json: - raise falcon.HTTPNotAcceptable( - 'This API only supports responses encoded as JSON.', - href='http://docs.examples.com/api/json') - if req.method in ('POST', 'PUT'): - if not req.content_type == 'application/json': - raise falcon.HTTPUnsupportedMediaType( - 'This API only supports requests encoded as JSON.', +class RequireJSON(object): + + def process_request(self, req, resp): + if not req.client_accepts_json: + raise falcon.HTTPNotAcceptable( + 'This API only supports responses encoded as JSON.', href='http://docs.examples.com/api/json') + if req.method in ('POST', 'PUT'): + if 'application/json' not in req.content_type: + raise falcon.HTTPUnsupportedMediaType( + 'This API only supports requests encoded as JSON.', + href='http://docs.examples.com/api/json') + + +class JSONTranslator(object): + + def process_request(self, req, resp): + # req.stream corresponds to the WSGI wsgi.input environ variable, + # and allows you to read bytes from the request body. + # + # See also: PEP 3333 + if req.content_length in (None, 0): + # Nothing to do + return -def deserialize(req, resp, resource, params): - # req.stream corresponds to the WSGI wsgi.input environ variable, - # and allows you to read bytes from the request body. - # - # See also: PEP 3333 - body = req.stream.read() - if not body: - raise falcon.HTTPBadRequest('Empty request body', - 'A valid JSON document is required.') + body = req.stream.read() + if not body: + raise falcon.HTTPBadRequest('Empty request body', + 'A valid JSON document is required.') + + try: + req.context['doc'] = json.loads(body.decode('utf-8')) + + except (ValueError, UnicodeDecodeError): + raise falcon.HTTPError(falcon.HTTP_753, + 'Malformed JSON', + 'Could not decode the request body. The ' + 'JSON was incorrect or not encoded as ' + 'UTF-8.') + + def process_response(self, req, resp): + if 'result' not in req.context: + return + + resp.body = json.dumps(req.context['result']) - try: - params['doc'] = json.loads(body.decode('utf-8')) - except (ValueError, UnicodeDecodeError): - raise falcon.HTTPError(falcon.HTTP_753, - 'Malformed JSON', - 'Could not decode the request body. The ' - 'JSON was incorrect or not encoded as UTF-8.') +def max_body(limit): + def hook(req, resp, resource, params): + length = req.content_length + if length is not None and length > limit: + msg = ('The size of the request is too large. The body must not ' + 'exceed ' + str(limit) + ' bytes in length.') -def serialize(req, resp, resource): - resp.body = json.dumps(req.context['doc']) + raise falcon.HTTPRequestEntityTooLarge( + 'Request body is too large', msg) + + return hook class ThingsResource: @@ -281,7 +312,6 @@ class ThingsResource: self.db = db self.logger = logging.getLogger('thingsapp.' + __name__) - @falcon.after(serialize) def on_get(self, req, resp, user_id): marker = req.get_param('marker') or '' limit = req.get_param_as_int('limit') or 50 @@ -304,13 +334,20 @@ class ThingsResource: # create a custom class that inherits from falcon.Request. This # class could, for example, have an additional 'doc' property # that would serialize to JSON under the covers. - req.context['doc'] = result + req.context['result'] = result resp.set_header('X-Powered-By', 'Small Furry Creatures') resp.status = falcon.HTTP_200 - @falcon.before(deserialize) - def on_post(self, req, resp, user_id, doc): + @falcon.before(max_body(64 * 1024)) + def on_post(self, req, resp, user_id): + try: + doc = req.context['doc'] + except KeyError: + raise falcon.HTTPBadRequest( + 'Missing thing', + 'A thing must be submitted in the request body.') + proper_thing = self.db.add_thing(doc) resp.status = falcon.HTTP_201 @@ -318,7 +355,11 @@ class ThingsResource: # Configure your WSGI server to load "things.app" (app is a WSGI callable) -app = falcon.API(before=[auth, check_media_type]) +app = falcon.API(middleware=[ + AuthMiddleware(), + RequireJSON(), + JSONTranslator(), +]) db = StorageEngine() things = ThingsResource(db) @@ -332,13 +373,12 @@ app.add_error_handler(StorageError, StorageError.handle) # send parts of an API off to a legacy system that hasn't been upgraded # yet, or perhaps is a single cluster that all data centers have to share. sink = SinkAdapter() -app.add_sink(sink, r'/v1/[charts|inventory]') +app.add_sink(sink, r'/search/(?Pddg|y)\Z') # Useful for debugging problems in your API; works with pdb.set_trace() if __name__ == '__main__': httpd = simple_server.make_server('127.0.0.1', 8000, app) httpd.serve_forever() - ``` ### Contributing ### diff --git a/README.rst b/README.rst index b7aeda844..f735a6079 100644 --- a/README.rst +++ b/README.rst @@ -192,17 +192,21 @@ Here is a more involved example that demonstrates reading headers and query para from wsgiref import simple_server import falcon + import requests class StorageEngine(object): + def get_things(self, marker, limit): - return [] + return [{'id': str(uuid.uuid4()), 'color': 'green'}] def add_thing(self, thing): - return {'id': str(uuid.uuid4())} + thing['id'] = str(uuid.uuid4()) + return thing class StorageError(Exception): + @staticmethod def handle(ex, req, resp, params): description = ('Sorry, couldn\'t write your thing to the ' @@ -213,82 +217,110 @@ Here is a more involved example that demonstrates reading headers and query para description) - class Proxy(object): - def forward(self, req): - return falcon.HTTP_503 - - class SinkAdapter(object): - def __init__(self): - self._proxy = Proxy() + engines = { + 'ddg': 'https://duckduckgo.com', + 'y': 'https://search.yahoo.com/search', + } - def __call__(self, req, resp, **kwargs): - resp.status = self._proxy.forward(req) - self.kwargs = kwargs + def __call__(self, req, resp, engine): + url = self.engines[engine] + params = {'q': req.get_param('q', True)} + result = requests.get(url, params=params) + resp.status = str(result.status_code) + ' ' + result.reason + resp.content_type = result.headers['content-type'] + resp.body = result.text - def token_is_valid(token, user_id): - return True # Suuuuuure it's valid... + class AuthMiddleware(object): - def auth(req, resp, params): - # Alternatively, use Talons or do this in WSGI middleware... - token = req.get_header('X-Auth-Token') + def process_request(self, req, resp): + token = req.get_header('X-Auth-Token') + project = req.get_header('X-Project-ID') - if token is None: - description = ('Please provide an auth token ' - 'as part of the request.') + if token is None: + description = ('Please provide an auth token ' + 'as part of the request.') - raise falcon.HTTPUnauthorized('Auth token required', - description, - href='http://docs.example.com/auth') + raise falcon.HTTPUnauthorized('Auth token required', + description, + href='http://docs.example.com/auth') - if not token_is_valid(token, params['user_id']): - description = ('The provided auth token is not valid. ' - 'Please request a new token and try again.') + if not self._token_is_valid(token, project): + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') - raise falcon.HTTPUnauthorized('Authentication required', - description, - href='http://docs.example.com/auth', - scheme='Token; UUID') + raise falcon.HTTPUnauthorized('Authentication required', + description, + href='http://docs.example.com/auth', + scheme='Token; UUID') + def _token_is_valid(self, token, project): + return True # Suuuuuure it's valid... - def check_media_type(req, resp, params): - if not req.client_accepts_json: - raise falcon.HTTPNotAcceptable( - 'This API only supports responses encoded as JSON.', - href='http://docs.examples.com/api/json') - if req.method in ('POST', 'PUT'): - if not req.content_type == 'application/json': - raise falcon.HTTPUnsupportedMediaType( - 'This API only supports requests encoded as JSON.', + class RequireJSON(object): + + def process_request(self, req, resp): + if not req.client_accepts_json: + raise falcon.HTTPNotAcceptable( + 'This API only supports responses encoded as JSON.', href='http://docs.examples.com/api/json') + if req.method in ('POST', 'PUT'): + if 'application/json' not in req.content_type: + raise falcon.HTTPUnsupportedMediaType( + 'This API only supports requests encoded as JSON.', + href='http://docs.examples.com/api/json') + + + class JSONTranslator(object): + + def process_request(self, req, resp): + # req.stream corresponds to the WSGI wsgi.input environ variable, + # and allows you to read bytes from the request body. + # + # See also: PEP 3333 + if req.content_length in (None, 0): + # Nothing to do + return - def deserialize(req, resp, resource, params): - # req.stream corresponds to the WSGI wsgi.input environ variable, - # and allows you to read bytes from the request body. - # - # See also: PEP 3333 - body = req.stream.read() - if not body: - raise falcon.HTTPBadRequest('Empty request body', - 'A valid JSON document is required.') + body = req.stream.read() + if not body: + raise falcon.HTTPBadRequest('Empty request body', + 'A valid JSON document is required.') - try: - params['doc'] = json.loads(body.decode('utf-8')) + try: + req.context['doc'] = json.loads(body.decode('utf-8')) + + except (ValueError, UnicodeDecodeError): + raise falcon.HTTPError(falcon.HTTP_753, + 'Malformed JSON', + 'Could not decode the request body. The ' + 'JSON was incorrect or not encoded as ' + 'UTF-8.') + + def process_response(self, req, resp): + if 'result' not in req.context: + return + + resp.body = json.dumps(req.context['result']) - except (ValueError, UnicodeDecodeError): - raise falcon.HTTPError(falcon.HTTP_753, - 'Malformed JSON', - 'Could not decode the request body. The ' - 'JSON was incorrect or not encoded as UTF-8.') + def max_body(limit): - def serialize(req, resp, resource): - resp.body = json.dumps(req.context['doc']) + def hook(req, resp, resource, params): + length = req.content_length + if length is not None and length > limit: + msg = ('The size of the request is too large. The body must not ' + 'exceed ' + str(limit) + ' bytes in length.') + + raise falcon.HTTPRequestEntityTooLarge( + 'Request body is too large', msg) + + return hook class ThingsResource: @@ -297,7 +329,6 @@ Here is a more involved example that demonstrates reading headers and query para self.db = db self.logger = logging.getLogger('thingsapp.' + __name__) - @falcon.after(serialize) def on_get(self, req, resp, user_id): marker = req.get_param('marker') or '' limit = req.get_param_as_int('limit') or 50 @@ -320,13 +351,20 @@ Here is a more involved example that demonstrates reading headers and query para # create a custom class that inherits from falcon.Request. This # class could, for example, have an additional 'doc' property # that would serialize to JSON under the covers. - req.context['doc'] = result + req.context['result'] = result resp.set_header('X-Powered-By', 'Small Furry Creatures') resp.status = falcon.HTTP_200 - @falcon.before(deserialize) - def on_post(self, req, resp, user_id, doc): + @falcon.before(max_body(64 * 1024)) + def on_post(self, req, resp, user_id): + try: + doc = req.context['doc'] + except KeyError: + raise falcon.HTTPBadRequest( + 'Missing thing', + 'A thing must be submitted in the request body.') + proper_thing = self.db.add_thing(doc) resp.status = falcon.HTTP_201 @@ -334,7 +372,11 @@ Here is a more involved example that demonstrates reading headers and query para # Configure your WSGI server to load "things.app" (app is a WSGI callable) - app = falcon.API(before=[auth, check_media_type]) + app = falcon.API(middleware=[ + AuthMiddleware(), + RequireJSON(), + JSONTranslator(), + ]) db = StorageEngine() things = ThingsResource(db) @@ -348,7 +390,7 @@ Here is a more involved example that demonstrates reading headers and query para # send parts of an API off to a legacy system that hasn't been upgraded # yet, or perhaps is a single cluster that all data centers have to share. sink = SinkAdapter() - app.add_sink(sink, r'/v1/[charts|inventory]') + app.add_sink(sink, r'/search/(?Pddg|y)\Z') # Useful for debugging problems in your API; works with pdb.set_trace() if __name__ == '__main__': diff --git a/doc/user/quickstart.rst b/doc/user/quickstart.rst index 5deb86c0a..dcd4b367e 100644 --- a/doc/user/quickstart.rst +++ b/doc/user/quickstart.rst @@ -75,17 +75,21 @@ parameters, handling errors, and working with request and response bodies. from wsgiref import simple_server import falcon + import requests class StorageEngine(object): + def get_things(self, marker, limit): - return [] + return [{'id': str(uuid.uuid4()), 'color': 'green'}] def add_thing(self, thing): - return {'id': str(uuid.uuid4())} + thing['id'] = str(uuid.uuid4()) + return thing class StorageError(Exception): + @staticmethod def handle(ex, req, resp, params): description = ('Sorry, couldn\'t write your thing to the ' @@ -96,82 +100,110 @@ parameters, handling errors, and working with request and response bodies. description) - class Proxy(object): - def forward(self, req): - return falcon.HTTP_503 - - class SinkAdapter(object): - def __init__(self): - self._proxy = Proxy() + engines = { + 'ddg': 'https://duckduckgo.com', + 'y': 'https://search.yahoo.com/search', + } + + def __call__(self, req, resp, engine): + url = self.engines[engine] + params = {'q': req.get_param('q', True)} + result = requests.get(url, params=params) - def __call__(self, req, resp, **kwargs): - resp.status = self._proxy.forward(req) - self.kwargs = kwargs + resp.status = str(result.status_code) + ' ' + result.reason + resp.content_type = result.headers['content-type'] + resp.body = result.text - def token_is_valid(token, user_id): - return True # Suuuuuure it's valid... + class AuthMiddleware(object): + def process_request(self, req, resp): + token = req.get_header('X-Auth-Token') + project = req.get_header('X-Project-ID') - def auth(req, resp, params): - # Alternatively, use Talons or do this in WSGI middleware... - token = req.get_header('X-Auth-Token') + if token is None: + description = ('Please provide an auth token ' + 'as part of the request.') - if token is None: - description = ('Please provide an auth token ' - 'as part of the request.') + raise falcon.HTTPUnauthorized('Auth token required', + description, + href='http://docs.example.com/auth') - raise falcon.HTTPUnauthorized('Auth token required', - description, - href='http://docs.example.com/auth') + if not self._token_is_valid(token, project): + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') - if not token_is_valid(token, params['user_id']): - description = ('The provided auth token is not valid. ' - 'Please request a new token and try again.') + raise falcon.HTTPUnauthorized('Authentication required', + description, + href='http://docs.example.com/auth', + scheme='Token; UUID') - raise falcon.HTTPUnauthorized('Authentication required', - description, - href='http://docs.example.com/auth', - scheme='Token; UUID') + def _token_is_valid(self, token, project): + return True # Suuuuuure it's valid... - def check_media_type(req, resp, params): - if not req.client_accepts_json: - raise falcon.HTTPNotAcceptable( - 'This API only supports responses encoded as JSON.', - href='http://docs.examples.com/api/json') + class RequireJSON(object): - if req.method in ('POST', 'PUT'): - if not req.content_type == 'application/json': - raise falcon.HTTPUnsupportedMediaType( - 'This API only supports requests encoded as JSON.', + def process_request(self, req, resp): + if not req.client_accepts_json: + raise falcon.HTTPNotAcceptable( + 'This API only supports responses encoded as JSON.', href='http://docs.examples.com/api/json') + if req.method in ('POST', 'PUT'): + if 'application/json' not in req.content_type: + raise falcon.HTTPUnsupportedMediaType( + 'This API only supports requests encoded as JSON.', + href='http://docs.examples.com/api/json') + + + class JSONTranslator(object): + + def process_request(self, req, resp): + # req.stream corresponds to the WSGI wsgi.input environ variable, + # and allows you to read bytes from the request body. + # + # See also: PEP 3333 + if req.content_length in (None, 0): + # Nothing to do + return + + body = req.stream.read() + if not body: + raise falcon.HTTPBadRequest('Empty request body', + 'A valid JSON document is required.') + + try: + req.context['doc'] = json.loads(body.decode('utf-8')) + + except (ValueError, UnicodeDecodeError): + raise falcon.HTTPError(falcon.HTTP_753, + 'Malformed JSON', + 'Could not decode the request body. The ' + 'JSON was incorrect or not encoded as ' + 'UTF-8.') + + def process_response(self, req, resp): + if 'result' not in req.context: + return - def deserialize(req, resp, resource, params): - # req.stream corresponds to the WSGI wsgi.input environ variable, - # and allows you to read bytes from the request body. - # - # See also: PEP 3333 - body = req.stream.read() - if not body: - raise falcon.HTTPBadRequest('Empty request body', - 'A valid JSON document is required.') + resp.body = json.dumps(req.context['result']) - try: - params['doc'] = json.loads(body.decode('utf-8')) - except (ValueError, UnicodeDecodeError): - raise falcon.HTTPError(falcon.HTTP_753, - 'Malformed JSON', - 'Could not decode the request body. The ' - 'JSON was incorrect or not encoded as UTF-8.') + def max_body(limit): + def hook(req, resp, resource, params): + length = req.content_length + if length is not None and length > limit: + msg = ('The size of the request is too large. The body must not ' + 'exceed ' + str(limit) + ' bytes in length.') - def serialize(req, resp, resource): - resp.body = json.dumps(req.context['doc']) + raise falcon.HTTPRequestEntityTooLarge( + 'Request body is too large', msg) + + return hook class ThingsResource: @@ -180,7 +212,6 @@ parameters, handling errors, and working with request and response bodies. self.db = db self.logger = logging.getLogger('thingsapp.' + __name__) - @falcon.after(serialize) def on_get(self, req, resp, user_id): marker = req.get_param('marker') or '' limit = req.get_param_as_int('limit') or 50 @@ -203,13 +234,20 @@ parameters, handling errors, and working with request and response bodies. # create a custom class that inherits from falcon.Request. This # class could, for example, have an additional 'doc' property # that would serialize to JSON under the covers. - req.context['doc'] = result + req.context['result'] = result resp.set_header('X-Powered-By', 'Small Furry Creatures') resp.status = falcon.HTTP_200 - @falcon.before(deserialize) - def on_post(self, req, resp, user_id, doc): + @falcon.before(max_body(64 * 1024)) + def on_post(self, req, resp, user_id): + try: + doc = req.context['doc'] + except KeyError: + raise falcon.HTTPBadRequest( + 'Missing thing', + 'A thing must be submitted in the request body.') + proper_thing = self.db.add_thing(doc) resp.status = falcon.HTTP_201 @@ -217,7 +255,11 @@ parameters, handling errors, and working with request and response bodies. # Configure your WSGI server to load "things.app" (app is a WSGI callable) - app = falcon.API(before=[auth, check_media_type]) + app = falcon.API(middleware=[ + AuthMiddleware(), + RequireJSON(), + JSONTranslator(), + ]) db = StorageEngine() things = ThingsResource(db) @@ -231,13 +273,9 @@ parameters, handling errors, and working with request and response bodies. # send parts of an API off to a legacy system that hasn't been upgraded # yet, or perhaps is a single cluster that all data centers have to share. sink = SinkAdapter() - app.add_sink(sink, r'/v1/[charts|inventory]') + app.add_sink(sink, r'/search/(?Pddg|y)\Z') # Useful for debugging problems in your API; works with pdb.set_trace() if __name__ == '__main__': httpd = simple_server.make_server('127.0.0.1', 8000, app) httpd.serve_forever() - - - - diff --git a/doc/user/tutorial.rst b/doc/user/tutorial.rst index 112137e67..bc95a65e4 100644 --- a/doc/user/tutorial.rst +++ b/doc/user/tutorial.rst @@ -3,10 +3,10 @@ Tutorial ======== -This page walks you through building an API for an image-sharing service. Along -the way, you will learn about Falcon's features and the terminology used by -the framework. You'll also learn how to query Falcon's docstrings, and get a -quick overview of the WSGI standard. +This page walks you through building an API for a simple image-sharing +service. Along the way, you will learn about Falcon's features and the +terminology used by the framework. You'll also learn how to query Falcon's +docstrings, and get a quick overview of the WSGI standard. .. include:: big-picture-snip.rst diff --git a/tests/test_example.py b/tests/test_example.py index f8d101902..90e1bec51 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -4,17 +4,21 @@ from wsgiref import simple_server import falcon +import requests class StorageEngine(object): + def get_things(self, marker, limit): - return [] + return [{'id': str(uuid.uuid4()), 'color': 'green'}] def add_thing(self, thing): - return {'id': str(uuid.uuid4())} + thing['id'] = str(uuid.uuid4()) + return thing class StorageError(Exception): + @staticmethod def handle(ex, req, resp, params): description = ('Sorry, couldn\'t write your thing to the ' @@ -25,82 +29,110 @@ def handle(ex, req, resp, params): description) -class Proxy(object): - def forward(self, req): - return falcon.HTTP_503 - - class SinkAdapter(object): - def __init__(self): - self._proxy = Proxy() + engines = { + 'ddg': 'https://duckduckgo.com', + 'y': 'https://search.yahoo.com/search', + } - def __call__(self, req, resp, **kwargs): - resp.status = self._proxy.forward(req) - self.kwargs = kwargs + def __call__(self, req, resp, engine): + url = self.engines[engine] + params = {'q': req.get_param('q', True)} + result = requests.get(url, params=params) + resp.status = str(result.status_code) + ' ' + result.reason + resp.content_type = result.headers['content-type'] + resp.body = result.text -def token_is_valid(token, user_id): - return True # Suuuuuure it's valid... +class AuthMiddleware(object): -def auth(req, resp, params): - # Alternatively, use Talons or do this in WSGI middleware... - token = req.get_header('X-Auth-Token') + def process_request(self, req, resp): + token = req.get_header('X-Auth-Token') + project = req.get_header('X-Project-ID') - if token is None: - description = ('Please provide an auth token ' - 'as part of the request.') + if token is None: + description = ('Please provide an auth token ' + 'as part of the request.') - raise falcon.HTTPUnauthorized('Auth token required', - description, - href='http://docs.example.com/auth') + raise falcon.HTTPUnauthorized('Auth token required', + description, + href='http://docs.example.com/auth') - if not token_is_valid(token, params['user_id']): - description = ('The provided auth token is not valid. ' - 'Please request a new token and try again.') + if not self._token_is_valid(token, project): + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') - raise falcon.HTTPUnauthorized('Authentication required', - description, - href='http://docs.example.com/auth', - scheme='Token; UUID') + raise falcon.HTTPUnauthorized('Authentication required', + description, + href='http://docs.example.com/auth', + scheme='Token; UUID') + def _token_is_valid(self, token, project): + return True # Suuuuuure it's valid... -def check_media_type(req, resp, params): - if not req.client_accepts_json: - raise falcon.HTTPNotAcceptable( - 'This API only supports responses encoded as JSON.', - href='http://docs.examples.com/api/json') - if req.method in ('POST', 'PUT'): - if not req.content_type == 'application/json': - raise falcon.HTTPUnsupportedMediaType( - 'This API only supports requests encoded as JSON.', +class RequireJSON(object): + + def process_request(self, req, resp): + if not req.client_accepts_json: + raise falcon.HTTPNotAcceptable( + 'This API only supports responses encoded as JSON.', href='http://docs.examples.com/api/json') + if req.method in ('POST', 'PUT'): + if 'application/json' not in req.content_type: + raise falcon.HTTPUnsupportedMediaType( + 'This API only supports requests encoded as JSON.', + href='http://docs.examples.com/api/json') + + +class JSONTranslator(object): + + def process_request(self, req, resp): + # req.stream corresponds to the WSGI wsgi.input environ variable, + # and allows you to read bytes from the request body. + # + # See also: PEP 3333 + if req.content_length in (None, 0): + # Nothing to do + return -def deserialize(req, resp, resource, params): - # req.stream corresponds to the WSGI wsgi.input environ variable, - # and allows you to read bytes from the request body. - # - # See also: PEP 3333 - body = req.stream.read() - if not body: - raise falcon.HTTPBadRequest('Empty request body', - 'A valid JSON document is required.') + body = req.stream.read() + if not body: + raise falcon.HTTPBadRequest('Empty request body', + 'A valid JSON document is required.') - try: - params['doc'] = json.loads(body.decode('utf-8')) + try: + req.context['doc'] = json.loads(body.decode('utf-8')) + + except (ValueError, UnicodeDecodeError): + raise falcon.HTTPError(falcon.HTTP_753, + 'Malformed JSON', + 'Could not decode the request body. The ' + 'JSON was incorrect or not encoded as ' + 'UTF-8.') + + def process_response(self, req, resp): + if 'result' not in req.context: + return + + resp.body = json.dumps(req.context['result']) - except (ValueError, UnicodeDecodeError): - raise falcon.HTTPError(falcon.HTTP_753, - 'Malformed JSON', - 'Could not decode the request body. The ' - 'JSON was incorrect or not encoded as UTF-8.') +def max_body(limit): -def serialize(req, resp, resource): - resp.body = json.dumps(req.context['doc']) + def hook(req, resp, resource, params): + length = req.content_length + if length is not None and length > limit: + msg = ('The size of the request is too large. The body must not ' + 'exceed ' + str(limit) + ' bytes in length.') + + raise falcon.HTTPRequestEntityTooLarge( + 'Request body is too large', msg) + + return hook class ThingsResource: @@ -109,7 +141,6 @@ def __init__(self, db): self.db = db self.logger = logging.getLogger('thingsapp.' + __name__) - @falcon.after(serialize) def on_get(self, req, resp, user_id): marker = req.get_param('marker') or '' limit = req.get_param_as_int('limit') or 50 @@ -132,13 +163,20 @@ def on_get(self, req, resp, user_id): # create a custom class that inherits from falcon.Request. This # class could, for example, have an additional 'doc' property # that would serialize to JSON under the covers. - req.context['doc'] = result + req.context['result'] = result resp.set_header('X-Powered-By', 'Small Furry Creatures') resp.status = falcon.HTTP_200 - @falcon.before(deserialize) - def on_post(self, req, resp, user_id, doc): + @falcon.before(max_body(64 * 1024)) + def on_post(self, req, resp, user_id): + try: + doc = req.context['doc'] + except KeyError: + raise falcon.HTTPBadRequest( + 'Missing thing', + 'A thing must be submitted in the request body.') + proper_thing = self.db.add_thing(doc) resp.status = falcon.HTTP_201 @@ -146,7 +184,11 @@ def on_post(self, req, resp, user_id, doc): # Configure your WSGI server to load "things.app" (app is a WSGI callable) -app = falcon.API(before=[auth, check_media_type]) +app = falcon.API(middleware=[ + AuthMiddleware(), + RequireJSON(), + JSONTranslator(), +]) db = StorageEngine() things = ThingsResource(db) @@ -160,7 +202,7 @@ def on_post(self, req, resp, user_id, doc): # send parts of an API off to a legacy system that hasn't been upgraded # yet, or perhaps is a single cluster that all data centers have to share. sink = SinkAdapter() -app.add_sink(sink, r'/v1/[charts|inventory]') +app.add_sink(sink, r'/search/(?Pddg|y)\Z') # Useful for debugging problems in your API; works with pdb.set_trace() if __name__ == '__main__': From 6beba38df1edeca34198f4c5eeebc9020bdd67e3 Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Thu, 22 Jan 2015 17:01:09 -0600 Subject: [PATCH 8/8] doc(guide): Improve and correct the prose across several documentation pages Edit the prose in the existing documentation for clarity and correctness. Closes #296 --- README.md | 3 --- doc/api/hooks.rst | 33 +++++++++++++++++++-------------- doc/community/contribute.rst | 2 +- doc/community/faq.rst | 6 ++---- doc/conf.py | 2 +- doc/user/big-picture-snip.rst | 6 +----- doc/user/intro.rst | 15 +++++++++------ doc/user/quickstart.rst | 1 + doc/user/tutorial.rst | 7 +++---- 9 files changed, 37 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 936afc26a..a54598624 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,6 @@ We have started documenting the library at http://falcon.readthedocs.org and we The docstrings in the Falcon code base are quite extensive, and we recommend keeping a REPL running while learning the framework so that you can query the various modules and classes as you have questions. -You can also check out [Zaqar's WSGI driver](https://github.com/openstack/zaqar/tree/master/zaqar/queues/transport/wsgi) to get a feel for how you might -leverage Falcon in building a REST API. - The Falcon community maintains a mailing list that you can use to share your ideas and ask questions about the framework. We use the appropriately minimalistic [Librelist](http://librelist.com/) to host the discussions. diff --git a/doc/api/hooks.rst b/doc/api/hooks.rst index 38d56ac08..2c45fbdfe 100644 --- a/doc/api/hooks.rst +++ b/doc/api/hooks.rst @@ -4,10 +4,11 @@ Hooks ===== Falcon supports both **before** and **after** hooks. You install a hook simply by -applying one of the decorators below either to an individual responder or +applying one of the decorators below, either to an individual responder or to an entire resource. -For example, suppose you had a hook like this: +For example, consider this hook that validates a POST request for +an image resource: .. code:: python @@ -16,7 +17,7 @@ For example, suppose you had a hook like this: msg = 'Image type not allowed. Must be PNG, JPEG, or GIF' raise falcon.HTTPBadRequest('Bad request', msg) -You would attach the hook to an ``on_post`` responder like so: +You would attach this hook to an ``on_post`` responder like so: .. code:: python @@ -24,22 +25,26 @@ You would attach the hook to an ``on_post`` responder like so: def on_post(self, req, resp): pass -Or, if you had a hook that you would like to applied to *all* -responders for a given resource, you could install the hook like this: +Or, suppose you had a hook that you would like to apply to *all* +responders for a given resource. In that case, you would simply +decorate the resource class: .. code:: python @falcon.before(extract_project_id) class Message(object): - pass - -And you can apply hooks globally by passing them into the API class -initializer (note that this does not require the use of a decorator): - -.. code:: python - - falcon.API(before=[extract_project_id]) - + def on_post(self, req, resp): + pass + + def on_get(self, req, resp): + pass + +Falcon middleware components can also be used to insert logic before and +after requests. Unlike hooks, however, middleware components are +triggered **globally** for all requests. This feature is +documented in the +:ref:`API class ` reference and the +:ref:`Quickstart ` example code. .. automodule:: falcon :members: before, after diff --git a/doc/community/contribute.rst b/doc/community/contribute.rst index ff1271861..9bba24652 100644 --- a/doc/community/contribute.rst +++ b/doc/community/contribute.rst @@ -50,7 +50,7 @@ doesn't make pyflakes sad. * Use whitespace to separate logical blocks of code and to improve readability. * Do not use single-character variable names except for trivial indexes when looping, or in mathematical expressions implementing well-known formulae. -* Heavily document code that is especially complex and/or clever. +* Heavily document code that is especially complex or clever! * When in doubt, optimize for readability. .. _napolean-flavored: http://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html#example-google-style-python-docstrings diff --git a/doc/community/faq.rst b/doc/community/faq.rst index 8ad57d4af..92a4ace00 100644 --- a/doc/community/faq.rst +++ b/doc/community/faq.rst @@ -17,9 +17,7 @@ simply wrap your api instance with a middleware app. For example: app = some_middleware.DoSomethingFancy(my_restful_service.api) -See also the `WSGI middleware example `_ given in PEP-3333. Note that use of Paste for wiring up -middleware is discouraged these days, because that package is not -well-maintained, and is incompatible with Python 3. +See also the `WSGI middleware example `_ given in PEP-3333. Why doesn't Falcon include X? @@ -46,7 +44,7 @@ have full access to the Request and Response objects. .. code:: python - def auth(req, resp, params): + def auth(req, resp, resource, params): token = req.get_header('X-Auth-Token') if token is None: diff --git a/doc/conf.py b/doc/conf.py index 87ddbe935..6e175aa2b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -55,7 +55,7 @@ # General information about the project. project = u'Falcon' -copyright = u'2014, Kurt Griffiths and Rackspace Hosting' +copyright = u'2015, Kurt Griffiths and Rackspace Hosting' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/user/big-picture-snip.rst b/doc/user/big-picture-snip.rst index c6b773832..a1a8f2cb7 100644 --- a/doc/user/big-picture-snip.rst +++ b/doc/user/big-picture-snip.rst @@ -1,10 +1,6 @@ The Big Picture --------------- -Falcon encourages composition over inheritance in order to extend the -functionality of the framework. This helps make applications easier to -maintain and refactor over time. - .. image:: ../_static/img/my-web-app.png :alt: Falcon-based web application architecture - :width: 600 \ No newline at end of file + :width: 600 diff --git a/doc/user/intro.rst b/doc/user/intro.rst index 912c07855..4a057b153 100644 --- a/doc/user/intro.rst +++ b/doc/user/intro.rst @@ -3,23 +3,26 @@ Introduction ============ -Falcon is a minimalist, high-performance web framework for building web services and app backends with Python. It's WSGI-based, and works great with Python 2.6, Python 2.7, Python 3.3, Python 3.4 and PyPy, giving you a wide variety of deployment options. +Falcon is a minimalist, high-performance web framework for building RESTful services and app backends with Python. Falcon works with any WSGI container that is compliant with PEP-3333, and works great with Python 2.6, Python 2.7, Python 3.3, Python 3.4 and PyPy, giving you a wide variety of deployment options. How is Falcon different? ------------------------ -First, Falcon is one of the fastest WSGI frameworks on the planet, and we are always trying to make it perform even better. When there is a conflict between saving the developer a few keystrokes and saving a few microseconds to serve a request, Falcon is strongly biased toward the latter. Falcon strives to strike a good balance between usability and speed. +First, Falcon is one of the fastest WSGI frameworks available. When there is a conflict between saving the developer a few keystrokes and saving a few microseconds to serve a request, Falcon is strongly biased toward the latter. That being said, Falcon strives to strike a good balance between usability and speed. -Second, Falcon is lean. It doesn't try to be everything to everyone, focusing instead on a single use case: HTTP APIs. Falcon doesn't include a template engine, form helpers, or an ORM (although those are easy enough to add yourself). When you sit down to write a web service with Falcon, you choose your own adventure in terms of async I/O, serialization, data access, etc. In fact, the only dependencies Falcon takes is on six, to make it easier to support both Python 2 and 3, and on mimeparse for handling complex Accept headers. +Second, Falcon is lean. It doesn't try to be everything to everyone, focusing instead on a single use case: HTTP APIs. Falcon doesn't include a template engine, form helpers, or an ORM (although those are easy enough to add yourself). When you sit down to write a web service with Falcon, you choose your own adventure in terms of async I/O, serialization, data access, etc. In fact, Falcon only has two dependencies: `six`_, to make it easier to support both Python 2 and 3, and `mimeparse`_ for handling complex Accept headers. Neither of these packages pull in any further dependencies of their own. -Third, Falcon eschews magic. When you use the framework, it's pretty obvious which inputs lead to which outputs. Also, it's blatantly obvious where variables originate. All this makes it easier for you and your posterity to reason about your code, even months (or years) after you wrote it. +Third, Falcon eschews magic. When you use the framework, it's pretty obvious which inputs lead to which outputs. Also, it's blatantly obvious where variables originate. All this makes it easier to reason about the code and to debug edge cases in large-scale deployments of your application. + +.. _`six`: http://pythonhosted.org/six/ +.. _`mimeparse`: https://code.google.com/p/mimeparse/ About Apache 2.0 ---------------- -Falcon is released under the terms of the `Apache 2.0 License`_. This means you can use it in your commercial applications without having to also open-source your own code. It also means that if someone happens to contribute code that is associated with a patent, you are granted a free license to use said patent. That's a pretty sweet deal. +Falcon is released under the terms of the `Apache 2.0 License`_. This means that you can use it in your commercial applications without having to also open-source your own code. It also means that if someone happens to contribute code that is associated with a patent, you are granted a free license to use said patent. That's a pretty sweet deal. Now, if you do make changes to Falcon itself, please consider contributing your awesome work back to the community. @@ -29,4 +32,4 @@ Now, if you do make changes to Falcon itself, please consider contributing your Falcon License -------------- - .. include:: ../../LICENSE \ No newline at end of file +.. include:: ../../LICENSE \ No newline at end of file diff --git a/doc/user/quickstart.rst b/doc/user/quickstart.rst index dcd4b367e..02671dd26 100644 --- a/doc/user/quickstart.rst +++ b/doc/user/quickstart.rst @@ -60,6 +60,7 @@ Then, in another terminal: $ curl localhost:8000/things +.. _quickstart-more-features: More Features ------------- diff --git a/doc/user/tutorial.rst b/doc/user/tutorial.rst index bc95a65e4..47e29b3da 100644 --- a/doc/user/tutorial.rst +++ b/doc/user/tutorial.rst @@ -3,10 +3,9 @@ Tutorial ======== -This page walks you through building an API for a simple image-sharing -service. Along the way, you will learn about Falcon's features and the -terminology used by the framework. You'll also learn how to query Falcon's -docstrings, and get a quick overview of the WSGI standard. +In this tutorial we'll walk through building an API for a simple image sharing +service. Along the way, we'll discuss Falcon's major features and introduce +the terminology used by the framework. .. include:: big-picture-snip.rst