flask-transmute¶
A transmute framework for flask. This framework provides:
- declarative generation of http handler interfaces by parsing function annotations
- validation and serialization to and from a variety of content types (e.g. json or yaml).
- validation and serialization to and from native python objects, using schematics.
- autodocumentation of all handlers generated this way, via swagger.
flask-transmute is provided as a flask extension, and can be included in the setup.py and/or requirements.txt for your service.
Here’s a brief example:
import flask_transmute
from flask import Flask, Blueprint
app = Flask(__name__)
blueprint = Blueprint("blueprint", __name__, url_prefix="/foo")
# creates an api that:
# * accepts multiple markup types like json and yaml
# * validates with input types that are specified
@flask_transmute.route(app, paths='/multiple')
# annotate types to tell flask-transmute what to verify
# the type as (default string)
@flask_transmute.annotate({"left": int, "right": int, "return": int})
def multiply(left, right):
return left * right
# if you use python 3.5+, you can annotate directly
# in the method signature.
@flask_transmute.route(app, paths='/multiply3')
def multiply_3(left: int, right: int) -> int:
return left + right
# blueprints are supported as well
@flask_transmute.route(blueprint, paths='/subroute')
@flask_transmute.annotate({"return": bool})
def subroute():
return True
app.register_blueprint(blueprint)
# finally, you can add a swagger json and a documentation page by:
flask_transmute.add_swagger(app, "/swagger.json", "/swagger")
app.run()
Legacy Implementation¶
flask-transmute 1.0 uses a completely different implementation of the transmute functionality based on transmute-core
Documentation for the pre-1.0 version can be found under the legacy section.
Contents:
Installing¶
flask-transmute can be installed via Pip, from PyPI:
$ pip install flask-transmute
Pip allows installation from source as well:
$ pip install git+https://github.com/toumorokoshi/flask-transmute.git#egg=flask-transmute
Routes¶
Example¶
Adding routes follows the flask pattern, with a decorator a decorator converting a function to a flask route
from flask_transmute import route, annotate
# define a GET endpoint, taking a query parameter integers left and right,
# which must be integers.
@route(app, paths="/multiply")
@annotate({"left": int, "right": int, "return": int})
def multiply(left, right):
return left * right
see transmute-core:function for more information on customizing transmute routes.
API Documentation¶
Serialization¶
See serialization in transmute-core.
Autodocumentation¶
You can use add_swagger(app, json_path, html_path) to add swagger documentation for all transmute routes.
flask_transmute.add_swagger(app, "/swagger.json", "/swagger")
API Reference¶
pre 1.0 documentation¶
flask-transmute is a flask extension that generates APIs from standard python functions and classes. Autodocumention is also provided via swagger.
Here’s a brief example:
import flask_transmute
from flask import Flask
route_set = flask_transmute.FlaskRouteSet()
class Pet(object):
def __init__(self, name, classification):
self.name = name
self.classification = classification
transmute_model = {
"properties": {
"name": {"type": str},
"classification": {"type": str}
},
"required": ["name", "classification"]
}
@staticmethod
def from_transmute_dict(model):
return Pet(model["name"], model["classification"])
@route_set.route("/add_pet")
@flask_transmute.updates
# python 2 doesn't support parameter annotations.
# instead, you can do
# @flask_transmute.annotate({"pet": Pet, "return": Pet})
def add_pet(pet: Pet) -> Pet:
animals.add(pet)
return pet
app = Flask(__name__)
route_set.init_app(app)
The example above creates a path /add_pet that:
- accepts POST as a method (due to flask_transmute.updates)
- requires a body containing the fields “name” and “type”
- returns a response {“success”: True, “response”: True}
You can find a more in-depth example here: https://github.com/toumorokoshi/flask-transmute/blob/master/examples/deck.py
In raw flask, the above is equivalent to:
import json
from flask import Flask, jsonify, request
app = Flask(__name__)
class ApiException(Exception):
pass
class Pet(object):
def __init__(self, name, classification):
self.name = name
self.classification = classification
@staticmethod
def from_dict(model):
return Pet(model["name"], model["classification"])
@staticmethod
def validate_pet_dict(model):
errors = []
if "name" not in model:
errors.append("name not in model!")
elif not isinstance(model["name"], str):
errors.append("expected a string for name. found: {0}".format(type(model["name"]))
if "classification" not in model:
errors.append("name not in model!")
elif not isinstance(model["classification"], str):
errors.append("expected a string for classification. found: {0}".format(type(model["classification"]))
return errors
def to_dict(self):
return {"name": self.name, "classification": self.classification}
@app.route("/add_pet", methods=["POST"])
def add_pet():
try:
if "json" in request.content_type:
request_args = json.loads(request.get_data().decode("UTF-8"))
else:
request_args = request.form
if "pet" not in request_args:
raise ApiException("pet field is required")
errors = Pet.validate_pet_dict(request_args["pet"])
if errors:
raise ApiException(str(errors))
pet = request_args["pet"]
pet_object = Pet.from_dict(model)
animals.add(pet_object)
return jsonify({"success": True, "result": pet_object.to_dict()})
except ApiException as e:
return jsonify({"success": False, "detail": str(e)})
Contents:
autodocumentation¶
Autodocumention is generated via a swagger spec: swagger. It is generated by instatiating a swagger object, then using the “init_app” method:
from flask_transmute.swagger import Swagger
from flask import Flask
route_set = flask_transmute.FlaskRouteSet()
@route_set.route("/api/is_programming_fun")
def is_programming_fun(language: str) -> bool:
"""
given a language, return True if programming
in that language is fun.
"""
return language.lower() == "python"
app = Flask(__name__)
route_set.init_app(app)
swagger = Swagger("myApi", "1.0", route_prefix="/api")
swagger.init_app(app)
init_app will attach two routes:
- <route_prefix>, which will be a swagger ui page with your route documentation.
- <route_prefix>/swagger.json, which will be the swagger spec itself.
decorators¶
flask-transmute attempts to extract as much information as possible from the function declaration itself. However, there are times when behaviour should be indicated that is not clear from the function signature.
to accomodate this, flask-transmute provides decorators to help describe additional behavior:
import flask_transmute
cards = []
# create indicates data is being created,
# correlated to a PUT request
@flask_transmute.creates
def create_card(card: str) -> bool:
cards.append(card)
return True
# updates indicates data is being updated,
# correlated to a POST request
@flask_transmute.updates
def update_card(old_card: str, new_card: str) -> bool:
if old_card in cards:
card_index = cards.index(old_card)
cards.remove(old_card)
cards.insert(card_index, new_card)
return True
return False
# deletes indicates data is being deleted,
# correlated to a DELETE request
@flask_transmute.deletes
def delete_card(card: str) -> bool:
if card in cards:
cards.remove(card)
return True
return False
# in Python 2, it is not possible to annotate functions. flask_transmute
# provides a decorator to help with that
@flask_transmute.creates
@flask_transume.annotates({"card": str, "return": bool})
def create_card(card):
cards.append(card)
return True
serialization¶
Due to the dynamic nature of Python objects, it’s necessary to provide some attributes on class declarations that flask-transmute uses when serializing to and from a dictionary.
Two attributes are required:
- a dictionary attribute “transmute_schema” that specifies the structure of the object.
- a static method “from_transmute_dict” that receives a dictionary and should return an instance of the class.
The transmute_schema property is very similar to json-schemas, with the exception of using Python type objects instead of strings to define types:
transmute_schema = {
"properties": {
"cards": {
"type": [Card]
}
"name": {"type": str}
},
"required": ["name"]
}
The following types may be used:
- [Type] (a list type)
- str
- bool
- NoneType
- int
- any class that is also serializable
The following keys are available:
- properties: this should be a dictionary of string keys and type declaration values, of the format {“type”: cls}.
- required: this should be a list of attributes on the object that are required.
Here’s a complete example:
class Card(object):
def __init__(self, name, description):
self.name = name
self.description = description
transmute_schema = {
"properties": {
"name": {"type": str},
"description": {"type": str}
},
"required": ["name", "description"]
}
@staticmethod
def from_transmute_dict(model):
return Card(model["name"], model["description"])
Migrating to flask-transmute 1.0+¶
Migrating to the new transmute framework has multiple advantages:
- better swagger compliance
- customizable validation frameworks, default is Schematics
- up-to-date Swagger UI
- lack of dependencies on other unrelated libraries (such as flask-restful)
Migrating includes the following steps:
- switch each model in transmute to schematics.
- replace each FlaskRouteSet() with a blueprint
- aggregating all decorators and routes to flask_transmute.route()
- replace Swagger and swagger.init_app with add_swagger
The steps are outlined in detail below.
1. Converting to Schematics¶
flask-transmute has dropped it’s proprietary json-schema based model definition, and adopted the Schematics library as the default serializer.
Note
the serializer is customizable. See TransmuteContext
flask_transmute.annotate() is still the correct function to use, to annotate rich types.
Primitive objects (str, int, float) are still supported. Complex objects should be represented as a Schematics schema instead:
# old style
class Pet(object):
def __init__(self, name, classification):
self.name = name
self.classification = classification
transmute_model = {
"properties": {
"name": {"type": str},
"classification": {"type": str}
},
"required": ["name", "classification"]
}
@staticmethod
def from_transmute_dict(model):
return Pet(model["name"], model["classification"])
@flask_transmute.annotate({"pet": Pet, "return": bool})
def my_method(pet):
return True
# new style
import flask_transmute
from schematics.models import Model
from schematics.types import StringType
class Pet(Model):
name = StringType(required=True)
classification = StringType(required=True)
@flask_transmute.annotate({"pet": Pet, "return": bool})
def my_method(pet):
return True
2. Replace FlaskRouteSet with blueprints¶
flask-transmute now supports blueprints natively, and there is no longer a need for a custom object such as FlaskRouteSet. However, the flask_transmute.route should still be used over the standard blueprint.route:
# before
import flask_transmute
route_set = flask_transmute.FlaskRouteSet()
@route_set.route("/is_programming_fun")
@flask_transmute.updates
@flask_transmute.annotate({"answers": bool, "return": bool})
def is_programming_fun(answer):
return True
route_set.init_app(app)
# after
from flask import Blueprint
import flask_transmute
blueprint = Blueprint("blueprint", __name__, url_prefix="/blueprint")
@flask_transmute.route(blueprint, paths="/is_programming_fun")
@flask_transmute.annotate({"answers": bool, "return": bool})
def is_programming_fun(answer):
return True
app.register_blueprint(blueprint)
3. aggregate route descriptors to route¶
flask-transmute now aggregates all decorators into a single one: flask_transmute.describe. All arguments passed into the new flask_transmute.route are also passed along to a describe() call:
# before
@route_set.route("/is_programming_fun")
@flask_transmute.updates
@flask_transmute.annotate({"answers": bool, "return": bool})
def is_programming_fun(answer):
return True
# after
@flask_transmute.route(app)
@flask_transmute.describe(paths="/is_programming_fun", methods=["POST"])
@flask_transmute.annotate({"answers": bool, "return": bool})
def is_programming_fun(answer):
return True
Even simpler, arguments to describe can be passed into route directly:
# after
@flask_transmute.route(app, paths="/is_programming_fun", methods=["POST"])
@flask_transmute.annotate({"answers": bool, "return": bool})
def is_programming_fun(answer):
return True
Warning
the new transmute syntax does not use the flask routing syntax, and uses the generic transmute-core path. Specifically, the path wildcard “/path/<var_name>” should be replaced with the wildcard “/path/{var_name}” instead.
4. replace init_swagger with add_swagger¶
Instead of instantiating and calling a swagger object, the add_swagger method should be used instead:
# before
from flask_transmute.swagger import Swagger
swagger = Swagger("myApi", "1.0", route_prefix="/api")
swagger.init_app(app)
# after
import flask_transmute
flask_transmute.add_swagger(app, "/api/swagger.json", "/api/",
title="myApi", version="1.0")
And you’re done! You can learn more about how to customize in this document, and the transmute-core docs.