Posted under misc by Sander with tag(s) python flask

I created a small Flask library for creating clean API endpoints. My main motivation was to prevent having to write code like this all the time:

@app.route('/json/<page_id:int>/', methods=['POST'])
def page(page_id):
    # get the limit parameter
    limit = request.json.get('limit', None)

    # validate the limit parameter
    if not isinstance(limit, int):
        return jsonify(...)

Instead, I’d like to write something like this:

@app.route('/json/<page_id:int>/', methods=['POST'])
@endpoint.api(
    parameter.api('limit', type=int, required=True)
)
def page(page_id, limit):
    return {"foo": "bar"}

As such, I’ve written Flask-YoloAPI which attempts to make things a litte more to my liking. It takes its inspiration from the way Google App Engine endpoints work, minus the classes.

Here’s the Github link

Examples

from flask_yoloapi import endpoint, parameter

@app.route('/api/login', methods=['GET', 'POST'])
@endpoint.api(
    parameter('username', type=str, required=True),
    parameter('password', type=str, required=True),
    parameter('remember', type=bool, required=False, default=False)
)
def login(username, password, remember):
    """
    Logs the user in.
    :param username: The username of the user
    :param password: The password of the user
    :param expiration: Session expiration time in seconds
    :return: The logged in message!
    """
    return "user logged in!"

The response:

{
    data: "user logged in!"
}

Return values

In the example above, a string was returned. The following types are also supported:

  • str, unicode, int, float, dict, list, datetime.
@app.route('/wishlist')
@endpoint.api(
    parameter('category', type=str, required=False)
)
def wishlist(category):
    if category == "cars":
        return ['volvo xc60', 'mclaren mp4-12c']
{
    "data": [
        "volvo xc60", 
        "mclaren mp4-12c"
    ]
}

HTTP status codes

To return different status codes, return a 2-length tuple with the second index being the status code itself.

@app.route('/create_foo')
@endpoint.api()
def view_function():
    return 'created', 201

Route parameters

You can still use Flask’s route parameters in conjunction with endpoint parameters.

@app.route('/hello/<name>')
@endpoint.api(
    parameter('age', type=int, required=True)
)
def hello(name, age):
    return {'name': name, 'age': age}

/hello/sander?age=27

{
    "data": {
        "age": 27, 
        "name": "sander"
    }
}

Default values

You can define default values for endpoint parameters via default.

@app.route('/hello/<name>')
@endpoint.api(
    parameter('age', type=int, required=False, default=10)
)
def hello(name, age):
    return {'name': name, 'age': age}

/hello/sander

{
    "data": {
        "age": 10, 
        "name": "sander"
    }
}

Type annotations

Parameter types are required, except when type annotations are in use.

A Python 3.5 example:

@app.route('/hello/', methods=['POST'])
@endpoint.api(
    parameter('age', required=True),
    parameter('name', required=True)
)
def hello(name: str, age: int):
    return {'name': name, 'age': age}

Python 2 equivalent:

@app.route('/hello/', methods=['POST'])
@endpoint.api(
    parameter('age', type=int, required=True),
    parameter('name', type=str, required=True)
)
def hello(name, age):
    return {'name': name, 'age': age}

Note that type annotations are only supported from Python 3.5 and upwards (PEP 484).

Custom validators

Additional parameter validation can be done by providing a validator function. This function takes 1 parameter; the input.

An Exception must be raised when the validation proves to be unsuccessful.

def custom_validator(value):
    if value > 120:
        raise Exception("you can't possibly be that old!")

@app.route('/hello/<name>')
@endpoint.api(
    parameter('age', type=int, required=True, validator=custom_validator)
)
def hello(name, age):
    return {'name': name, 'age': age}

/hello/sander?age=130

{
    "data": "parameter 'age' error: you can't possibly be that old!"
}

If you need more flexibility regarding incoming types use the yoloapi.types.ANY type.

Parameter handling

This library is rather opportunistic about gathering incoming parameters, as it will check in the following 3 places:

  • request.args
  • request.json
  • request.form

Datetime format

To output datetime objects in ISO 8601 format (which are trivial to parse in Javascript via Date.parse()), use a custom JSON encoder.

from datetime import date
from flask.json import JSONEncoder

class ApiJsonEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (date, datetime)):
            return obj.isoformat()
        return super(ApiJsonEncoder, self).default(obj)

app = Flask(__name__)
app.json_encoder = CustomJSONEncoder

Error handling

When the view function itself raises an exception, a JSON response is generated that includes:

  • The error message
  • Docstring of the view function
  • HTTP 500

This error response is also generated when endpoint requirements are not met.

{
    data: "argument 'password' is required",
    docstring: {
        help: "Logs the user in.",
        return: "The logged in message!",
        params: {
            username: {
                help: "The username of the user",
                required: true,
                type: "str"
                }
            },
        ...

License

MIT.