doc(guide): Update quickstart example code

Update the README and quickstart example code to showcase the new
middleware feature. Also tweak other parts of the example code,
as needed, to be in sync with the latest updates to the framework.

Partially implements #296
This commit is contained in:
Kurt Griffiths
2014-12-24 18:25:47 -06:00
parent 6ff79451c2
commit 87236e8c1c
5 changed files with 460 additions and 298 deletions

188
README.md
View File

@@ -169,24 +169,27 @@ $ curl localhost:8000/things
Here is a more involved example that demonstrates reading headers and query parameters, handling errors, and working with request and response bodies. Here is a more involved example that demonstrates reading headers and query parameters, handling errors, and working with request and response bodies.
```python ```python
import json import json
import logging import logging
import uuid import uuid
from wsgiref import simple_server from wsgiref import simple_server
import falcon import falcon
import requests
class StorageEngine(object): class StorageEngine(object):
def get_things(self, marker, limit): def get_things(self, marker, limit):
return [] return [{'id': str(uuid.uuid4()), 'color': 'green'}]
def add_thing(self, thing): def add_thing(self, thing):
return {'id': str(uuid.uuid4())} thing['id'] = str(uuid.uuid4())
return thing
class StorageError(Exception): class StorageError(Exception):
@staticmethod @staticmethod
def handle(ex, req, resp, params): def handle(ex, req, resp, params):
description = ('Sorry, couldn\'t write your thing to the ' description = ('Sorry, couldn\'t write your thing to the '
@@ -197,82 +200,110 @@ class StorageError(Exception):
description) description)
class Proxy(object):
def forward(self, req):
return falcon.HTTP_503
class SinkAdapter(object): class SinkAdapter(object):
def __init__(self): engines = {
self._proxy = Proxy() 'ddg': 'https://duckduckgo.com',
'y': 'https://search.yahoo.com/search',
}
def __call__(self, req, resp, **kwargs): def __call__(self, req, resp, engine):
resp.status = self._proxy.forward(req) url = self.engines[engine]
self.kwargs = kwargs 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): class AuthMiddleware(object):
return True # Suuuuuure it's valid...
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): class RequireJSON(object):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
if token is None: def process_request(self, req, resp):
description = ('Please provide an auth token ' if not req.client_accepts_json:
'as part of the request.') raise falcon.HTTPNotAcceptable(
'This API only supports responses encoded as JSON.',
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.',
href='http://docs.examples.com/api/json') href='http://docs.examples.com/api/json')
if req.method in ('POST', 'PUT'):
def deserialize(req, resp, resource, params): if 'application/json' not in req.content_type:
# req.stream corresponds to the WSGI wsgi.input environ variable, raise falcon.HTTPUnsupportedMediaType(
# and allows you to read bytes from the request body. 'This API only supports requests encoded as JSON.',
# href='http://docs.examples.com/api/json')
# 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.')
def serialize(req, resp, resource): class JSONTranslator(object):
resp.body = json.dumps(req.context['doc'])
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: class ThingsResource:
@@ -281,7 +312,6 @@ class ThingsResource:
self.db = db self.db = db
self.logger = logging.getLogger('thingsapp.' + __name__) self.logger = logging.getLogger('thingsapp.' + __name__)
@falcon.after(serialize)
def on_get(self, req, resp, user_id): def on_get(self, req, resp, user_id):
marker = req.get_param('marker') or '' marker = req.get_param('marker') or ''
limit = req.get_param_as_int('limit') or 50 limit = req.get_param_as_int('limit') or 50
@@ -304,13 +334,20 @@ class ThingsResource:
# create a custom class that inherits from falcon.Request. This # create a custom class that inherits from falcon.Request. This
# class could, for example, have an additional 'doc' property # class could, for example, have an additional 'doc' property
# that would serialize to JSON under the covers. # 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.set_header('X-Powered-By', 'Small Furry Creatures')
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
@falcon.before(deserialize) @falcon.before(max_body(64 * 1024))
def on_post(self, req, resp, user_id, doc): 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) proper_thing = self.db.add_thing(doc)
resp.status = falcon.HTTP_201 resp.status = falcon.HTTP_201
@@ -318,7 +355,11 @@ class ThingsResource:
# Configure your WSGI server to load "things.app" (app is a WSGI callable) # 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() db = StorageEngine()
things = ThingsResource(db) things = ThingsResource(db)
@@ -332,13 +373,12 @@ app.add_error_handler(StorageError, StorageError.handle)
# send parts of an API off to a legacy system that hasn't been upgraded # 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. # yet, or perhaps is a single cluster that all data centers have to share.
sink = SinkAdapter() 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() # Useful for debugging problems in your API; works with pdb.set_trace()
if __name__ == '__main__': if __name__ == '__main__':
httpd = simple_server.make_server('127.0.0.1', 8000, app) httpd = simple_server.make_server('127.0.0.1', 8000, app)
httpd.serve_forever() httpd.serve_forever()
``` ```
### Contributing ### ### Contributing ###

View File

@@ -192,17 +192,21 @@ Here is a more involved example that demonstrates reading headers and query para
from wsgiref import simple_server from wsgiref import simple_server
import falcon import falcon
import requests
class StorageEngine(object): class StorageEngine(object):
def get_things(self, marker, limit): def get_things(self, marker, limit):
return [] return [{'id': str(uuid.uuid4()), 'color': 'green'}]
def add_thing(self, thing): def add_thing(self, thing):
return {'id': str(uuid.uuid4())} thing['id'] = str(uuid.uuid4())
return thing
class StorageError(Exception): class StorageError(Exception):
@staticmethod @staticmethod
def handle(ex, req, resp, params): def handle(ex, req, resp, params):
description = ('Sorry, couldn\'t write your thing to the ' description = ('Sorry, couldn\'t write your thing to the '
@@ -213,82 +217,110 @@ Here is a more involved example that demonstrates reading headers and query para
description) description)
class Proxy(object):
def forward(self, req):
return falcon.HTTP_503
class SinkAdapter(object): class SinkAdapter(object):
def __init__(self): engines = {
self._proxy = Proxy() 'ddg': 'https://duckduckgo.com',
'y': 'https://search.yahoo.com/search',
}
def __call__(self, req, resp, **kwargs): def __call__(self, req, resp, engine):
resp.status = self._proxy.forward(req) url = self.engines[engine]
self.kwargs = kwargs 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): class AuthMiddleware(object):
return True # Suuuuuure it's valid...
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): class RequireJSON(object):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
if token is None: def process_request(self, req, resp):
description = ('Please provide an auth token ' if not req.client_accepts_json:
'as part of the request.') raise falcon.HTTPNotAcceptable(
'This API only supports responses encoded as JSON.',
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.',
href='http://docs.examples.com/api/json') href='http://docs.examples.com/api/json')
if req.method in ('POST', 'PUT'):
def deserialize(req, resp, resource, params): if 'application/json' not in req.content_type:
# req.stream corresponds to the WSGI wsgi.input environ variable, raise falcon.HTTPUnsupportedMediaType(
# and allows you to read bytes from the request body. 'This API only supports requests encoded as JSON.',
# href='http://docs.examples.com/api/json')
# 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.')
def serialize(req, resp, resource): class JSONTranslator(object):
resp.body = json.dumps(req.context['doc'])
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: class ThingsResource:
@@ -297,7 +329,6 @@ Here is a more involved example that demonstrates reading headers and query para
self.db = db self.db = db
self.logger = logging.getLogger('thingsapp.' + __name__) self.logger = logging.getLogger('thingsapp.' + __name__)
@falcon.after(serialize)
def on_get(self, req, resp, user_id): def on_get(self, req, resp, user_id):
marker = req.get_param('marker') or '' marker = req.get_param('marker') or ''
limit = req.get_param_as_int('limit') or 50 limit = req.get_param_as_int('limit') or 50
@@ -320,13 +351,20 @@ Here is a more involved example that demonstrates reading headers and query para
# create a custom class that inherits from falcon.Request. This # create a custom class that inherits from falcon.Request. This
# class could, for example, have an additional 'doc' property # class could, for example, have an additional 'doc' property
# that would serialize to JSON under the covers. # 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.set_header('X-Powered-By', 'Small Furry Creatures')
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
@falcon.before(deserialize) @falcon.before(max_body(64 * 1024))
def on_post(self, req, resp, user_id, doc): 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) proper_thing = self.db.add_thing(doc)
resp.status = falcon.HTTP_201 resp.status = falcon.HTTP_201
@@ -334,7 +372,11 @@ Here is a more involved example that demonstrates reading headers and query para
# Configure your WSGI server to load "things.app" (app is a WSGI callable) # 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() db = StorageEngine()
things = ThingsResource(db) things = ThingsResource(db)
@@ -348,7 +390,7 @@ Here is a more involved example that demonstrates reading headers and query para
# send parts of an API off to a legacy system that hasn't been upgraded # 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. # yet, or perhaps is a single cluster that all data centers have to share.
sink = SinkAdapter() 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() # Useful for debugging problems in your API; works with pdb.set_trace()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -75,17 +75,21 @@ parameters, handling errors, and working with request and response bodies.
from wsgiref import simple_server from wsgiref import simple_server
import falcon import falcon
import requests
class StorageEngine(object): class StorageEngine(object):
def get_things(self, marker, limit): def get_things(self, marker, limit):
return [] return [{'id': str(uuid.uuid4()), 'color': 'green'}]
def add_thing(self, thing): def add_thing(self, thing):
return {'id': str(uuid.uuid4())} thing['id'] = str(uuid.uuid4())
return thing
class StorageError(Exception): class StorageError(Exception):
@staticmethod @staticmethod
def handle(ex, req, resp, params): def handle(ex, req, resp, params):
description = ('Sorry, couldn\'t write your thing to the ' description = ('Sorry, couldn\'t write your thing to the '
@@ -96,82 +100,110 @@ parameters, handling errors, and working with request and response bodies.
description) description)
class Proxy(object):
def forward(self, req):
return falcon.HTTP_503
class SinkAdapter(object): class SinkAdapter(object):
def __init__(self): engines = {
self._proxy = Proxy() 'ddg': 'https://duckduckgo.com',
'y': 'https://search.yahoo.com/search',
}
def __call__(self, req, resp, **kwargs): def __call__(self, req, resp, engine):
resp.status = self._proxy.forward(req) url = self.engines[engine]
self.kwargs = kwargs 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): class AuthMiddleware(object):
return True # Suuuuuure it's valid...
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): class RequireJSON(object):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
if token is None: def process_request(self, req, resp):
description = ('Please provide an auth token ' if not req.client_accepts_json:
'as part of the request.') raise falcon.HTTPNotAcceptable(
'This API only supports responses encoded as JSON.',
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.',
href='http://docs.examples.com/api/json') href='http://docs.examples.com/api/json')
if req.method in ('POST', 'PUT'):
def deserialize(req, resp, resource, params): if 'application/json' not in req.content_type:
# req.stream corresponds to the WSGI wsgi.input environ variable, raise falcon.HTTPUnsupportedMediaType(
# and allows you to read bytes from the request body. 'This API only supports requests encoded as JSON.',
# href='http://docs.examples.com/api/json')
# 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.')
def serialize(req, resp, resource): class JSONTranslator(object):
resp.body = json.dumps(req.context['doc'])
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: class ThingsResource:
@@ -180,7 +212,6 @@ parameters, handling errors, and working with request and response bodies.
self.db = db self.db = db
self.logger = logging.getLogger('thingsapp.' + __name__) self.logger = logging.getLogger('thingsapp.' + __name__)
@falcon.after(serialize)
def on_get(self, req, resp, user_id): def on_get(self, req, resp, user_id):
marker = req.get_param('marker') or '' marker = req.get_param('marker') or ''
limit = req.get_param_as_int('limit') or 50 limit = req.get_param_as_int('limit') or 50
@@ -203,13 +234,20 @@ parameters, handling errors, and working with request and response bodies.
# create a custom class that inherits from falcon.Request. This # create a custom class that inherits from falcon.Request. This
# class could, for example, have an additional 'doc' property # class could, for example, have an additional 'doc' property
# that would serialize to JSON under the covers. # 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.set_header('X-Powered-By', 'Small Furry Creatures')
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
@falcon.before(deserialize) @falcon.before(max_body(64 * 1024))
def on_post(self, req, resp, user_id, doc): 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) proper_thing = self.db.add_thing(doc)
resp.status = falcon.HTTP_201 resp.status = falcon.HTTP_201
@@ -217,7 +255,11 @@ parameters, handling errors, and working with request and response bodies.
# Configure your WSGI server to load "things.app" (app is a WSGI callable) # 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() db = StorageEngine()
things = ThingsResource(db) things = ThingsResource(db)
@@ -231,13 +273,9 @@ parameters, handling errors, and working with request and response bodies.
# send parts of an API off to a legacy system that hasn't been upgraded # 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. # yet, or perhaps is a single cluster that all data centers have to share.
sink = SinkAdapter() 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() # Useful for debugging problems in your API; works with pdb.set_trace()
if __name__ == '__main__': if __name__ == '__main__':
httpd = simple_server.make_server('127.0.0.1', 8000, app) httpd = simple_server.make_server('127.0.0.1', 8000, app)
httpd.serve_forever() httpd.serve_forever()

View File

@@ -3,10 +3,10 @@
Tutorial Tutorial
======== ========
This page walks you through building an API for an image-sharing service. Along This page walks you through building an API for a simple image-sharing
the way, you will learn about Falcon's features and the terminology used by service. Along the way, you will learn about Falcon's features and the
the framework. You'll also learn how to query Falcon's docstrings, and get a terminology used by the framework. You'll also learn how to query Falcon's
quick overview of the WSGI standard. docstrings, and get a quick overview of the WSGI standard.
.. include:: big-picture-snip.rst .. include:: big-picture-snip.rst

View File

@@ -4,17 +4,21 @@ import uuid
from wsgiref import simple_server from wsgiref import simple_server
import falcon import falcon
import requests
class StorageEngine(object): class StorageEngine(object):
def get_things(self, marker, limit): def get_things(self, marker, limit):
return [] return [{'id': str(uuid.uuid4()), 'color': 'green'}]
def add_thing(self, thing): def add_thing(self, thing):
return {'id': str(uuid.uuid4())} thing['id'] = str(uuid.uuid4())
return thing
class StorageError(Exception): class StorageError(Exception):
@staticmethod @staticmethod
def handle(ex, req, resp, params): def handle(ex, req, resp, params):
description = ('Sorry, couldn\'t write your thing to the ' description = ('Sorry, couldn\'t write your thing to the '
@@ -25,82 +29,110 @@ class StorageError(Exception):
description) description)
class Proxy(object):
def forward(self, req):
return falcon.HTTP_503
class SinkAdapter(object): class SinkAdapter(object):
def __init__(self): engines = {
self._proxy = Proxy() 'ddg': 'https://duckduckgo.com',
'y': 'https://search.yahoo.com/search',
}
def __call__(self, req, resp, **kwargs): def __call__(self, req, resp, engine):
resp.status = self._proxy.forward(req) url = self.engines[engine]
self.kwargs = kwargs 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): class AuthMiddleware(object):
return True # Suuuuuure it's valid...
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): class RequireJSON(object):
# Alternatively, use Talons or do this in WSGI middleware...
token = req.get_header('X-Auth-Token')
if token is None: def process_request(self, req, resp):
description = ('Please provide an auth token ' if not req.client_accepts_json:
'as part of the request.') raise falcon.HTTPNotAcceptable(
'This API only supports responses encoded as JSON.',
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.',
href='http://docs.examples.com/api/json') href='http://docs.examples.com/api/json')
if req.method in ('POST', 'PUT'):
def deserialize(req, resp, resource, params): if 'application/json' not in req.content_type:
# req.stream corresponds to the WSGI wsgi.input environ variable, raise falcon.HTTPUnsupportedMediaType(
# and allows you to read bytes from the request body. 'This API only supports requests encoded as JSON.',
# href='http://docs.examples.com/api/json')
# 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.')
def serialize(req, resp, resource): class JSONTranslator(object):
resp.body = json.dumps(req.context['doc'])
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: class ThingsResource:
@@ -109,7 +141,6 @@ class ThingsResource:
self.db = db self.db = db
self.logger = logging.getLogger('thingsapp.' + __name__) self.logger = logging.getLogger('thingsapp.' + __name__)
@falcon.after(serialize)
def on_get(self, req, resp, user_id): def on_get(self, req, resp, user_id):
marker = req.get_param('marker') or '' marker = req.get_param('marker') or ''
limit = req.get_param_as_int('limit') or 50 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 # create a custom class that inherits from falcon.Request. This
# class could, for example, have an additional 'doc' property # class could, for example, have an additional 'doc' property
# that would serialize to JSON under the covers. # 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.set_header('X-Powered-By', 'Small Furry Creatures')
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
@falcon.before(deserialize) @falcon.before(max_body(64 * 1024))
def on_post(self, req, resp, user_id, doc): 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) proper_thing = self.db.add_thing(doc)
resp.status = falcon.HTTP_201 resp.status = falcon.HTTP_201
@@ -146,7 +184,11 @@ class ThingsResource:
# Configure your WSGI server to load "things.app" (app is a WSGI callable) # 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() db = StorageEngine()
things = ThingsResource(db) 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 # 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. # yet, or perhaps is a single cluster that all data centers have to share.
sink = SinkAdapter() 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() # Useful for debugging problems in your API; works with pdb.set_trace()
if __name__ == '__main__': if __name__ == '__main__':