Merge pull request #412 from kgriffs/issues/296
Edit docs for correctness and clarity
This commit is contained in:
207
README.md
207
README.md
@@ -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 ###
|
||||
|
||||
203
README.rst
203
README.rst
@@ -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__':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
130
falcon/api.py
130
falcon/api.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user