madness

wsgi microframework suitable for building modular DRY RESTful APIs


Keywords
http, http-server, json-api, lambda-functions, modular, python, python-3, python-library, rest-api, restful-api, wsgi, wsgi-framework
License
MIT
Install
pip install madness==0.5.0

Documentation

madness

Madness orchestrates the HTTP request-response cycle using context functions to build abstractions and route functions to transform abstractions into a HTTP responses.

It is built upon the fabulous werkzeug routing system.

Principles

Don't repeat yourself

Dependency inversion principle

Do One Thing and Do It Well.

Installing

$ pip install -U madness

A Simple Example

from madness import Madness, response

app = Madness()

@app.index
def hello():
    return response(['Hello, world!'])

if __name__ == '__main__':
    app.run()

Routing

@app.route(*paths, methods=[], context=[], origin=None, headers=[])

option description
*paths relative paths, defaults to the decorated function name
methods list of allowed http methods
context list of extra context functions see #Context
origin allowed origin: * or list of urls
headers allowed request headers: list of header names
wsgi set to True if the route implements a WSGI interface

convenience methods for @app.route

you can still use options with these!

@app.get, @app.post, @app.put, @app.delete, @app.patch, @app.options

RESTful routes

inspired by Ruby on Rails' resources

https://gist.github.com/alexpchin/09939db6f81d654af06b

https://medium.com/@shubhangirajagrawal/the-7-restful-routes-a8e84201f206

decorator path method
@app.index {path} GET
@app.new new{path} GET
@app.create {path} POST
@app.show /:id{path} GET
@app.edit /:id/edit{path} GET
@app.update /:id{path} PATCH/PUT
@app.destroy /:id{path} DELETE

AWS Lambda

see also: RequestResponse

from madness import json

@json.schema
class EventSchema():
    x: int = 1

@app.lambda_handler
def process(event: EventSchema):
    return {'y': event['x'] + 2}

if you annotate the event with a marshmallow schema, it is automatically validated :)

handling routing errors

@app.error(404)
def my404handler():
    return response(['not found'], status = 404)

Modules

from madness import Madness, response

app = Madness()

module = Madness()

@module.route
def thing():
    return response(['hello!'])

app.extend(module) # now app has /thing

app.extend(module, 'prefix') # now app has /prefix/thing

app.extend(module, context=False) # add the routes but not the context

if __name__ == '__main__':
  app.run()

Context

madness.context contains the abstractions created by the previous contexts

use @context to build abstractions for your low-level modules @context and @route

e.g.

  @context authenticate the HTTP request `context.username = 'xyz'`

  @context get database connection `context.database = Database()`

  @context(database) use database connection `context.data = database.myobjects.find(context.id)`

  @show(data) convert data to HTTP response `return json.response(data)`

rule args are added to context

e.g. @app.route('path/<myvar>') creates context.myvar

Basic Context Functions

from madness import context, json

@app.context
def before_request():
    "could do anything here, so let's add a variable to the context!"
    context.x = 2

@app.context
def continue_processing(x):
    "define context.y based on context.x!"
    context.y = x * 3 # 6

@app.route
def double(y):
    "doubles context.y and sends it as a JSON response"
    return json.response(y * 2) # 12

Advanced Context Generators

a context has full access to the request/response/exceptions

the response/exception is bubbled through the context handlers

@app.context
def advanced_context():
    # before_request
    if request.headers.get('x-api-key') != 'valid-api-key':
        # abort
        yield json.response({'message': 'invalid api key'}, status = 403)
    else:
        # run remaining context functions and the route endpoint (if not aborted)
        try:
            response = yield

        except MyException as exception:
            yield json.response({'message': exception.message}, status = 500)

        else:
            # modify the response headers
            response.headers['x-added-by-context'] = 'value'

            # abort
            yield json.response('we decided to not send the original response, isn\'t that weird?')

        finally:
            # after_request
            pass