Merge pull request #412 from kgriffs/issues/296

Edit docs for correctness and clarity
This commit is contained in:
Kurt Griffiths
2015-01-22 20:40:45 -04:00
18 changed files with 687 additions and 447 deletions

207
README.md
View File

@@ -27,19 +27,29 @@ 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 ###
> 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**
@@ -85,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.
@@ -159,24 +166,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 '
@@ -187,82 +197,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 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.')
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.')
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 auth(req, resp, params):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
class RequireJSON(object):
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')
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')
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.',
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')
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.')
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.')
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')
def serialize(req, resp, resource):
resp.body = json.dumps(req.context['doc'])
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
resp.body = json.dumps(req.context['result'])
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.')
raise falcon.HTTPRequestEntityTooLarge(
'Request body is too large', msg)
return hook
class ThingsResource:
@@ -271,7 +309,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
@@ -294,13 +331,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
@@ -308,7 +352,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)
@@ -322,13 +370,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/(?P<engine>ddg|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 ###

View File

@@ -41,22 +41,33 @@ 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
~~~~~~~
.. 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**
@@ -181,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 '
@@ -202,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 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.')
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.')
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 auth(req, resp, params):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
class RequireJSON(object):
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')
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')
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.',
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')
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.')
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.')
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')
def serialize(req, resp, resource):
resp.body = json.dumps(req.context['doc'])
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
resp.body = json.dumps(req.context['result'])
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.')
raise falcon.HTTPRequestEntityTooLarge(
'Request body is too large', msg)
return hook
class ThingsResource:
@@ -286,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
@@ -309,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
@@ -323,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)
@@ -337,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/(?P<engine>ddg|y)\Z')
# Useful for debugging problems in your API; works with pdb.set_trace()
if __name__ == '__main__':

View File

@@ -4,19 +4,20 @@ 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
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)
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
def on_post(self, req, resp):
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_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 <api>` reference and the
:ref:`Quickstart <quickstart-more-features>` example code.
.. automodule:: falcon
:members: before, after

View File

@@ -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

View File

@@ -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 <http://legacy.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides>`_ 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 <http://legacy.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides>`_ 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:

View File

@@ -55,7 +55,7 @@ master_doc = 'index'
# 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

View File

@@ -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
:width: 600

View File

@@ -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 <http://www.pip-installer.org/en/latest/installing.html>`_
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
-------------------------

View File

@@ -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
.. include:: ../../LICENSE

View File

@@ -60,6 +60,7 @@ Then, in another terminal:
$ curl localhost:8000/things
.. _quickstart-more-features:
More Features
-------------
@@ -75,17 +76,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 +101,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, **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 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.')
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.')
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 auth(req, resp, params):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
class RequireJSON(object):
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')
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')
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.',
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')
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.')
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.')
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')
def serialize(req, resp, resource):
resp.body = json.dumps(req.context['doc'])
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
resp.body = json.dumps(req.context['result'])
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.')
raise falcon.HTTPRequestEntityTooLarge(
'Request body is too large', msg)
return hook
class ThingsResource:
@@ -180,7 +213,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 +235,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 +256,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 +274,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/(?P<engine>ddg|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()

View File

@@ -3,10 +3,9 @@
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.
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

View File

@@ -27,22 +27,19 @@ import falcon.status_codes as status
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 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):
@@ -59,6 +56,11 @@ class API(object):
\"""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
@@ -110,11 +112,12 @@ class API(object):
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)
@@ -263,7 +266,7 @@ class API(object):
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
@@ -277,26 +280,27 @@ class API(object):
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.
/das/{thing}
For example, given the following template::
A PUT request to "/das/code" would be routed to::
/user/{name}
def on_put(self, req, resp, thing):
A PUT request to "/user/kgriffs" would be routed to::
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
@@ -313,11 +317,16 @@ class API(object):
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)``.
@@ -333,8 +342,9 @@ class API(object):
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``).
"""
@@ -348,28 +358,33 @@ class API(object):
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.
"""
@@ -394,6 +409,25 @@ class API(object):
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

View File

@@ -26,14 +26,15 @@ 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::
def do_something(req, resp, params):
def do_something(req, resp, resource, params):
try:
params['id'] = int(params['id'])
except ValueError:
@@ -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

View File

@@ -31,7 +31,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
@@ -61,7 +61,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
@@ -192,7 +192,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.
@@ -215,14 +215,14 @@ class NoRepresentation(object):
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

View File

@@ -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 @@ class Request(object):
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 @@ class Request(object):
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 @@ class Request(object):
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 @@ class Request(object):
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 @@ class Request(object):
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 @@ class Request(object):
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 @@ class Request(object):
['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 @@ class Request(object):
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.

View File

@@ -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
@@ -39,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
@@ -144,11 +157,12 @@ class Response(object):
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

View File

@@ -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:

View File

@@ -4,17 +4,21 @@ 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 '
@@ -25,82 +29,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 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.')
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.')
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 auth(req, resp, params):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
class RequireJSON(object):
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')
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')
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.',
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')
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.')
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.')
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')
def serialize(req, resp, resource):
resp.body = json.dumps(req.context['doc'])
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
resp.body = json.dumps(req.context['result'])
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.')
raise falcon.HTTPRequestEntityTooLarge(
'Request body is too large', msg)
return hook
class ThingsResource:
@@ -109,7 +141,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
@@ -132,13 +163,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
@@ -146,7 +184,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)
@@ -160,7 +202,7 @@ 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/(?P<engine>ddg|y)\Z')
# Useful for debugging problems in your API; works with pdb.set_trace()
if __name__ == '__main__':