ENOSUCHBLOG

Programming, philosophy, pedaling.


Introducing cazart

Aug 18, 2019

Tags: programming, devblog

Yet another module/library anouncement, this time for Python: cazart.

cazart solves an issue that I run into repeatedly: I want to expose some functionality via a small HTTP API that speaks only JSON, but don't want to do any manual payload validation1.

I usually end up using JSON schema (or better yet, a nice wrapper like schema) in a pattern like this:

from flask import Flask, request, jsonify
from schema import SchemaError

from .schemas import WhateverSchema


app = Flask(__name__)


@app.route("/whatever", methods=["POST"])
def whatever():
    res = request.get_json()
    try:
        WhateverSchema.validate(res)
    except SchemaError as e:
        return (e.code, 400)

    ...
    return jsonify(success=True)

But this is tedious, and separates two things that (I think) are semantically bound2: route and payload validation.

Enter cazart. Replace Flask(__name__) with Cazart(__name__) and you're more or less done:

from cazart import Cazart
from flask import jsonify
from .schemas import WhateverSchema


app = Cazart(__name__)


@app.route("/whatever", schema=WhateverSchema)
def whatever(res):
    print(f"validated payload {res}")
    return jsonify(success=True)

Observe that, unlike Flask, cazart will pass the (validated) payload as the first argument to the route handler. The underlying request can still be accessed through Flask's request.

By default, cazart will handle validation and serialization errors via an internal handler, which really just spits back an HTTP 400 with some useful debugging information.

This can be overridden by passing the error keyword to the route decorator:

from cazart import validation_error


def validation_failed():
    # This is treated like a normal route handler, so Flask methods
    # like abort and jsonify work as expected.
    # validation_error is a thread-local exposed by cazart, containing
    # a string describing the failure. bad_json is also available.
    return (validation_error, 500)


@app.route("/whatever", schema=WhateverSchema, error=validation_failed)
def whatever(res):
    return jsonify(success=True)

cazart is also aware of HTTP methods, and allows you to dispatch to different schemas based on the incoming method. Just pass a dict of method -> schema, and cazart will tell the underlying router to handle every method specified.

from .schemas import GetWhatever, PostWhatever

@app.route("/whatever", schema={"GET": GetWhatever, "POST": PostWhatever})
def whatever(res):
    if request.method == "GET":
        ...
    elif request.method == "POST":
        ...
    return jsonify(success=True)

Since cazart is a thin wrapper over Flask, you can use normal Flask methods within cazart-specified routes. Everything should just work (tm).

You can also define non-validated routes via app.flask, for endpoints that either aren't JSON or don't require validation:

# This is identical to @app.route in a normal Flask app
@app.flask.route("/danger")
def danger():
    print("danger! cazart isn't validating the payload for this route!")
    return jsonify(success=True)

...and everything will work as expected.

You can find runnable examples of cazart's behavior in each of these cases in the examples directory under the repository.

Installation

cazart can be installed via pip. It requires Python 3.6 or newer:

pip3 install cazart

Future work

schema is really an excellent library. It's also (in principle) format-agnostic, since it validates the resultant Python data structure instead of the JSON itself.

As a result, it might be nice to extend cazart to support custom deserializers at some point. This could probably be accomplished by introducing an additional keyword, which in turn would supply a function that takes request.data (or similar) and returns a Python data structure.


  1. For the usual reasons: flaky, missed edge cases, keeping up-to-date, hard to maintain, &c. 

  2. In these sorts of JSON-payload APIs, anyways.