Usage¶
The sections below detail how to fully use this module, along with context for design decisions made during development of the plugin.
In the beginning …¶
There was only Flask. Well, really … Werkzeug.
For context, let’s use a toy Flask application to illustrate some of the principles throughout the documentation. First, we’re going to define our application using normal Flask conventions. We’re also going to highlight some of the potential boilerplate that this approach requires:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_occam import Occam
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
class Item(db.Model):
__tablename__ = 'item'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False, unique=True, index=True)
email = db.Column(db.String(255))
@app.route('/items', methods=['GET'])
def all_items():
if request.method == 'GET': ## boilerplate
items = db.session.query(Item).all()
return jsonify([
dict(id=x.id, name=x.name)
for x in items
])
elif request.method == 'POST': ## boilerplate
email = request.json['email'] ## boilerplate
if not re.match(email, r".+@.+"): ## boilerplate
raise BadRequest ## boilerplate
item = Item(**request.json)
db.session.add(item)
db.session.commit() ## boilerplate
return jsonify(
id=item.id,
name=item.name
)
@app.route('/items/<int:ident>', methods=['GET', 'PUT', 'DELETE'])
def single_item(ident):
item = db.session.query(Item)\ ## boilerplate
.filter_by(id=ident).first() ## boilerplate
if not item: ## boilerplate
raise NotFound ## boilerplate
if request.method == 'GET': ## boilerplate
return jsonify(
id=item.id,
name=item.name
)
elif request.method == 'PUT': ## boilerplate
for key, value in request.json.items():
if key == 'email': ## boilerplate
if not re.match(value, r".+@.+"): ## boilerplate
raise BadRequest ## boilerplate
setattr(item, key, value)
db.session.commit() ## boilerplate
return jsonify(
id=item.id,
name=item.name
)
elif request.method == 'DELETE': ## boilerplate
db.session.delete(item)
db.session.commit() ## boilerplate
return jsonfify(msg='Deleted item')
A Cleaner Approach¶
Now, let’s use Flask-Occum
to clean up some of the boilerplate:
from wtforms import validators
from flask_occum import Occum
from flask_occum import transactional, validate, log, paginate
# initialize plugin
occum = Occum(app, db)
# endpoints
@app.route('/items')
class Items(object):
@paginate(limit=50, total=Item.count)
def get(self, limit, offset):
"""
GET /items
"""
items = Item.all(limit=limit, offset=offset)
return [x.json() for x in items], 200
@validate(name=str)
@transactional
@log.info('Created new user with name {name}')
def post(self):
"""
POST /items
"""
item = Item.create(**request.json)
return item.json(), 201
@app.route('/items/<id(Item):item>')
class SingleItem(object):
def get(self, item):
"""
GET /items/:id
"""
return item.json(), 200
@validate(
name=optional(str),
url=optional(validators.URL())
)
@transactional
@log.info('Changed metadata for item {item.name}')
def put(self, item):
"""
PUT /items/:id
"""
item.update(**request.json)
return item.json(), 200
@transactional
def delete(self, item):
"""
DELETE /items/:id
"""
item.delete()
return jsonify(msg='Deleted item'), 204
As you can see above, a good deal of the original boilerplate was removed, and although there is a similar number of lines of code, the readability (and by extension maintainability) is much better. The Occam example also includes additional utilities like payload validation, request action logging, and automatic pagination, which the original example didn’t provide (and would require a lot of code to produce).
Each of the utilities shown above are explained in greater detail throughout the documentation. This is example is mainly meant to give readers a feel for how application development changes with the extension.
Endpoint Documentation¶
Another benefit of using a class-based approach to request processing is that it enables you to include clear and concise documentation for your endpoints in the docstrings for each request method. This allows developers to easily generate API documentation for their application using the sphinx autodoc
functionality. Here’s a docstring-ified version of the example provided in the overview section of the documentation:
@app.route('/items')
class Items(object):
@paginate(limit=50, total=Item.count)
def get(self, limit, offset):
"""
GET /items
Query for existing item in application database.
Parameters:
limit (str): (optional) Return limit for query.
offset (str): (optional) Offset for querying results.
Response:
List of item objects. See GET /items/:id for
information on return payloads.
Status:
Success: 200 Created
Missing: 404 Not Found
"""
items = Item.all(limit=limit, offset=offset)
return [x.json() for x in items], 200
@validate(name=str)
@transactional
@log.info('Created new user {name}')
def post(self):
"""
POST /items
Query for existing item in application database.
Arguments:
id (int): Identifier for item.
Parameters:
name (str): Name for item
Response:
id (int): Identifier for item.
name (str): Item name.
url (str): Item URL.
Status:
Success: 201 Created
Missing: 404 Not Found
Failure: 422 Invalid Request
"""
item = Item.create(**request.json)
return item.json(), 201
@app.route('/items/<id(Item):item>')
class SingleItem(object):
def get(self, item):
"""
GET /items/:id
Query for existing item in application database.
Arguments:
id (int): Identifier for item.
Response:
id (int): Identifier for item.
name (str): Item name.
Status:
Success: 200 OK
Missing: 404 Not Found
"""
return jsonify(id=item.id, name=item.name), 200
@validate(
name=optional(str),
url=optional(validators.URL())
)
@transactional
@log.info('Changed metadata for item {item.name}')
def put(self, item):
"""
PUT /items/:id
Update existing item in application database.
Arguments:
id (int): Identifier for item.
Parameters:
name (str): (optional) Name for item
url (str): (optional) URL for item
Response:
id (int): Identifier for item.
name (str): Item name.
url (str): Item url.
Status:
Success: 200 OK
Missing: 404 Not Found
Failure: 422 Invalid Request
"""
item.update(**request.json)
return item.json(), 200
@transactional
def delete(self, item):
"""
DELETE /items/:id
Delete existing item in application database.
Arguments:
id (int): Identifier for item.
Status:
Success: 204 No Content
Missing: 404 Not Found
"""
item.delete()
return jsonify(msg='Deleted item'), 204
Nice, right? Defining your APIs like the above helps with code clarity, and forces developers to develop good habits when working on new endpoints. Sphinx can automatically generate html documentation from these docstrings using the autodoc
extension. Here’s an example of how to include auto-documentation for API handlers in your sphinx docs:
.. autoclass:: app.models.Item
:members:
In addition, you can have Flask-Occum automatically serve REST-based documentation for your endpoints by enabling the OCCAM_AUTODOC_ENABLED
configuration option. With that configuration option set to True
, you can retrieve endpoint documentation via request:
~$ curl -X GET http://localhost:5000/docs/items/:id
<pre>
GET /items/:id
Query for existing item in application database.
Arguments:
id (int): Identifier for item.
Response:
id (int): Identifier for item.
name (str): Item name.
Status:
Success: 200 OK
Missing: 404 Not Found
</pre>
Custom Request Handlers¶
Along with class-based request handling, you can also create custom classes for special endpoint handling. By default, Flask-Occum comes with two additional handlers:
- ActionHandler - Dispatch actions encoded in a URL (
POST /api/item/:id/:action
) to specific class methods. This is particularly useful for actions likearchive
or other model-specific functionality that needs to take place. - QueryHandler - Dispatch nested sub-queries encoded in a URL (
GET /api/item/:id/:query
) to specific lass melthods. This is useful for queries likestatus
or other model specific querying that needs to be available.
Here’s an example of using the ActionHandler helper class for processing endpoints that submit specific server-side actions (QueryHandler uses a very similar API):
from flask_occum import ActionHandler
@app.route('/items/<id(Item):item>/<action>')
class ItemActions(ActionHandler):
def archive(self, item):
# code to archive item whenever
# POST /items/:id/archive is submitted.
return
def unarchive(self, item):
# code to unarchive item whenever
# POST /items/:id/unarchive is submitted.
return
URL Processors¶
Above, we alluded to a custom url processor that automatically queries for objects of class Item
. Without this URL processor, querying for the item and checking if it exists creates boilerplate that permeates the entire codebase:
@app.route('/items/<int:ident>', methods=['GET'])
def get_item(ident):
item = db.session.query(Item)\ ## boilerplate
.filter_by(id=ident).first() ## boilerplate
if not item: ## boilerplate
raise NotFound
return item.json()
With the URL processor included with this extension, all of the querying and raising NotFound
errors is automatically managed when a request comes in:
@app.route('/items/<id(Item):item>')
def get_item(item):
return item.json()
When a request comes through, the item
argument is automatically transformed into an Item
object by the url processor, removing the need to always query the database and raise relevant errors. You can also do the same with any other object in the database. For example:
@app.route('/users/<id(User):user>/items/<id(Item):item>')
def get_user_item(user, item):
pass
Like before, user
is transformed into is a User
object, and item
is transformed into an Item
object. If neither object exists, a NotFound
error will be raised.
Using Blueprints¶
Flask-Occum is designed for seamlessly integrating with Flask, without changing much of how the app is configured or structured. The only Flask-y convention that needs to be slightly altered is how Blueprints
are used.
Instead of:
from flask import Blueprint
blueprint = Blueprint('blueprint_page', __name__, template_folder='templates'))
@blueprint.route('/test')
def test():
pass
You just import Blueprint
from flask_occam
:
from flask_occam import Blueprint
blueprint = Blueprint('blueprint_page', __name__, template_folder='templates'))
@blueprint.route('/test')
def test():
pass
Otherwise, the developer experience is the exact same.
Decorators¶
@validate
¶
With any large-scale web application, establishing a client-server contract for requests is incredibly important for keeping development organized and code clean. This extension provides a mechanism for defining endpoint contracts in a declarative way, which increases developer awareness of what’s happening in the application, and reduces the need for boilerplate code to validate payload data.
With the @validate
decorator, you could make payload validation as simple as built-in types:
@api.route('/items', methods=['POST'])
@validate(
string_param=str,
int_param=int,
float_param=float
)
def create_item():
pass
You can also use this decorator on API functions, if you want to structure your application so that request handling is dispatched to API functions. The @validate
decorator will check all function arguments according to their expected contract:
@validate(
string_param=str,
int_param=int,
float_param=float
)
def process_item(string_param, int_param, float_param):
pass
When calling this function, if the inputs aren’t specified according to the validation rules, and explicit error will be raised:
>>> process_item('test', 'test', 'test')
ValueError: Invalid arguments specified.
Errors:
float_param:
- Invalid type. Expecting `<class 'float'>`.
int_param:
- Invalid type. Expecting `<class 'int'>`.
In addition to supporting built-in types, the @validate
decorator also supports validators from the WTForms library. For example, to create custom validators for an email and password (with confirmation), you can do something like the following:
from wtforms import Form, StringField, PasswordField, validators
# defining validators
email = StringField('Email Address', [
validators.DataRequired(),
validators.Email(),
])
password = PasswordField('Password', [
validators.DataRequired(),
validators.Length(min=4, max=25),
validators.EqualTo('confirm', message='Passwords must match.')
])
confirm = PasswordField('Confirmation')
# endpoint
@validate(
email=email,
password=password,
confirm=confirm
)
@app.route('/login', methods=['GET'])
def login():
pass
In this example, whenever the /login
endpoint is hit with a payload, the @validate
decorator will automatically check if the data contains a valid email, and a password between 4 and 25 characters with a matching confirmation.
Finally, if you want to configure your form object separately, you can do so. Here’s an example of using the @validate
decorator with a Form
object directly:
from wtforms import Form
# form
class LoginForm(Form):
email = StringField('Email Address', [
validators.DataRequired(),
validators.Email(),
])
password = PasswordField('Password', [
validators.DataRequired(),
validators.Length(min=4, max=25),
validators.EqualTo('confirm', message='Passwords must match.')
])
confirm = PasswordField('Confirmation')
# endpoint
@validate(LoginForm)
@app.route('/login', methods=['GET'])
def login():
pass
And to really hammer in the point, here is an example with mixed types and validators, with nested validation:
from wtforms.validators import Email, NumberRange
@validate(
email=Email(), # email address
name=str, # string
tags=optional([str]), # optional list of strings
info=dict( # dictionary with nested validation
age=NumberRange( # 0 < age < 120
min=0,
max=120
),
nickname=optional(str) # optional string
)
)
def create_user(email, name, tags=None, info=None):
pass
As you can see above, optional validation can occur for parameters wrapped in with the optional
function. This is useful for PUT
requests where updates don’t need to happen on every field during every request. To explicitly make all parameters in the validation block optional, you can use the @validate.optional
decorator:
@validate.optional(
name=str, # optional string
tags=[str], # optional list of strings
)
def update_user(name=None, tags=None):
pass
This will only perform validation if known keys are specified in the request payload.
@log
¶
Logging in flask is already dead-simple, and this decorator mainly just provides an orthogonal route to doing logging in a consistent way. With the @log
decorator from this package, you can define high-level logging after an endpoint or API method is called, specifying the log information to the decorator:
@log.info('my_function was called')
def my_function():
pass
In addition, strings in logging are automatically formatted with function arguments and payload arguments, so you can include string formatting with keywords in the log directly:
@log.info("Created item with name {name}")
def create_item(name):
pass
If your application is configured to use Flask-Login
, you can include user information in the logs as well:
@log.debug("User {user.name} created item with name {name}")
def create_item(name):
pass
@paginate
¶
Applications serving lots of data often need a mechanism for paginating requests, so that the server doesn’t get overloaded with bulk requests. This plugin provides a decorator to automatically provide pagination information in the response header:
@app.route("/items", methods=['GET'])
@paginate(limit=50, total=Item.count)
def get_items():
items = Item.all(
limit=request.args['limit'],
offset=request.args['offset']
)
return [item.json() for item in items], 200
Request arguments added automatically by the @paginate
decorator are as follows:
- limit - The number of elements to paginate by.
- total - The total number of elements available in the database. This can be either a number or a
callable
.
Behind the scenes, this decorator will automatically set limit
and offset
request arguments, which developers can use when constructing a response. In the example above, if a request is made to /items
, limit
will be set to 50, and offset
will be set to 0. Response headers detailing the next request to make for more data will also automatically be set. See below for an example:
~$ curl -v -X GET http://localhost:5000/items
> GET /items HTTP/1.1
> User-Agent: curl/7.16.4 (i386-apple-darwin9.0) libcurl/7.16.4 OpenSSL/0.9.7l zlib/1.2.3
> Accept: */*
>
< HTTP/1.1 206 Partial Content
< Content-Type: application/json; charset=UTF-8
< X-Total-Count: 540
< Link: <http://localhost:5000/items?limit=50&offset=51>; rel="next"
<http://localhost:5000/items?limit=50&offset=501>; rel="last"
<
[
{'id': 1, 'name': 'one'},
{'id': 2, 'name': 'two'}
...
]
In addition to the X-Total-Count
and Link
header values, the decorator will also change the response code to 206 Partial Content
if the request is not the last request for retrieving data.
@transactional
¶
The @transactional
decorator is a tool for automatically managing transactions as requests are processed. If the application produces any error during a response, db.session.rollback()
is automatically called before the request finishes processing:
@transactional
def do_something():
item = Item(name='test')
db.session.add(item)
item.url = 1 / 0 ## raises error, forcing a database rollback
return
After the function executes, the flushed changes will also automatically be committed via db.session.commit()
. In total, the decorator doesn’t provide much on top of what SQLAlchemy already provides, but gives developers a nice wrapper to keep their transactional code clean.
SQLAlchemy Extensions¶
Similarly to the @log
decorator, this module simply provides an orthogonal mechanism for interacting with the database outside of heavily utilizing db.session
from Flask-SQLAlchemy
. However, this type of usage is in no way required, and you can continue to use db.session
if that’s the way you prefer to interact with the database. Here are some examples of CRUD operations using the extensions:
## Create
# without
item = Item(
name='test',
url='http://localhost:5000/items/1'
)
db.session.add(item)
# with
item = Item.create(
name='test'
url='http://localhost:5000/items/1'
)
## Read
# without
item = db.session.query(Item).filter_by(id=1).first()
items = db.session.query(Item).limit(5).offset(5).all()
# with
item = Item.get(1)
items = Item.all(limit=5, offset=5)
## Update
# without
item.name = 'test2'
item.url = None
db.session.add(item)
# with
item.update(name='test2', url=None)
## Delete
# without
db.session.delete(item)
# with
item.delete()
To enable these extensions when using the plugin, developers must instantiate the Flask-Occum
with a reference to the Flask-SQLAlchemy
plugin. Here’s an example of doing this:
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
occam = Occam(app, db)
However, as previously stated, passing in the Flask-SQLAlchemy plugin instance is not required to use the tools in this extension.
Configuration¶
The following configuration values exist for Flask-Occam. Flask-Occam loads these values from your main Flask config which can be populated in various ways. Note that some of those cannot be modified after the database engine was created so make sure to configure as early as possible and to not modify them at runtime.
Configuration Keys¶
A list of configuration keys currently understood by the extension:
OCCAM_LOG_USER_FORMAT |
The name of the current_user available
when using the @log decorator. Defaults
to user . |
OCCAM_AUTODOC_ENABLED |
Whether or not to enable the api auto- documentation feature. |
OCCAM_AUTODOC_PREFIX |
URL Prefix for auto-documentation entpoint. |
OCCAM_LOG_USER_FORMAT |
Name name for {user} formatter in @log decorator. |
OCCAM_LOG_DEFAULT_LEVEL |
Default log level for @log decorator. |