Using in Flask

doctor provides some helpers to for usage in a flask-restful application. You can find an example in app.py. To run this application, you’ll first need to install both doctor, flask, and flask-restful. Then run:

python app.py

The application will be running on http://127.0.0.1:5000.

Doctor Types

doctor types are classes that represent your request data and your responses. Each parameter of your logic function must specify a type hint which is a sublcass of one of the builtin doctor types. These types perform validation on the request parameters passed to the logic function. See Types for more information.

from doctor import types


# doctor provides helper functions to easily define simple types.
Body = types.string('Note body', example='body')
Done = types.boolean('Marks if a note is done or not.', example=False)
NoteId = types.integer('Note ID', example=1)
Status = types.string('API status')
NoteType = types.enum('The type of note', enum=['quick', 'detailed'],
                      example='quick')


# You can also inherit from type classes to create more complex types.
class Note(types.Object):
    description = 'A note object'
    additional_properties = False
    properties = {
        'note_id': NoteId,
        'body': Body,
        'done': Done,
    }
    required = ['body', 'done', 'note_id']
    example = {
        'body': 'Example Body',
        'done': True,
        'note_id': 1,
    }


Notes = types.array('Array of notes', items=Note, example=[Note.example])

Logic Functions

Next, we’ll also need some logic functions. doctor’s create_routes() generates HTTP handler methods that wrap normal Python callables, so the code can focus on more logic and less on HTTP. These handlers and HTTP handler methods will be automatically generated for you based on your defined routes.

The logic function signature will be used to determine what the request parameters are for the route/HTTP method and to determine which are required. Each parameter must specify a type hint which is a subclass of one of the builtin doctor types. These types perform validation on the request parameters passed to the logic function. See Types for more information. Any argument without a default value is considered required while others are optional. For example in the create_note function below, body would be a required request parameter and done would be an optional request parameter.

Any parameters that don’t validate will raise a HTTP400Exception. This exception will contain all validation errors and missing required properties. If there is more than one error, you can access them from the exception’s errobj attribute.

To abstract out the HTTP layer in logic functions, doctor provides custom exceptions which will be converted to the correct HTTP Exception by the library. See the Error Classes documentation for more information on which exception your code should raise.


note = {'note_id': 1, 'body': 'Example body', 'done': True}


# Note the type annotations on this function definition. This tells Doctor how
# to parse and validate parameters for routes attached to this logic function.
# The return type annotation will validate the response conforms to an
# expected definition in development environments.  In non-development
# environments a warning will be logged.
def get_note(note_id: NoteId, note_type: NoteType) -> Note:
    """Get a note by ID."""
    if note_id != 1:
        raise NotFoundError('Note does not exist')
    return note


def get_notes() -> Notes:
    """Get a list of notes."""
    return [note]


def create_note(body: Body, done: Done=False) -> Note:
    """Create a new note."""
    return {'note_id': 2,
            'body': body,
            'done': done}


def update_note(note_id: NoteId, body: Body=None, done: Done=None) -> Note:
    """Update an existing note."""
    if note_id != 1:
        raise NotFoundError('Note does not exist')
    new_note = note.copy()
    if body is not None:
        new_note['body'] = body
    if done is not None:
        new_note['done'] = done
    return new_note


def delete_note(note_id: NoteId):
    """Delete an existing note."""
    if note_id != 1:
        raise NotFoundError('Note does not exist')


def status() -> Status:
    return 'Notes API v1.0.0'


Creating Routes

Routes map a url to one or more HTTP methods which each map to a specific logic function. We define our routes by instantiating a Route. A Route requires 2 arguments. The first is the URL-matching pattern e.g. /foo/<int:foo_id>/. The second is a tuple of allowed HTTPMethod s for the matching pattern: get(), post(), put() and delete().

The HTTP method functions take one required argument which is the logic function to call when the http method for that uri is called.


routes = (
    Route('/', methods=(
        get(status),), heading='API Status'),
    Route('/note/', methods=(
        get(get_notes, title='Retrieve List'),
        post(create_note)), handler_name='NoteListHandler', heading='Notes (v1)'
    ),
    Route('/note/<int:note_id>/', methods=(
        delete(delete_note),
        get(get_note),
        put(update_note)), heading='Notes (v1)'
    ),
)

We then create our Flask app and add our created resources to it. These resources are created by calling create_routes() with our routes we defined above.


app = Flask('Doctor example')

api = Api(app)
for route, resource in create_routes(routes):
    api.add_resource(resource, route)

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

Passing Request Body as an Object to a Logic Function

If you need to pass the entire request body as an object like parameter instead of specifying each individual key in the logic function, you can specify which type the body should conform to when defining your route.

# Define the type the request body should conform to.
from doctor.types import integer, Object, string

class FooObject(Object):
    description = 'A foo.'
    properties = {
        'name': string('The name of the foo.'),
        'id': integer('The ID of the foo.'),
    }
    required = ['id']

# Define your logic function as normal.
def update_foo(foo: FooObject):
    print(foo['name'], foo['id'])
    # ...

# Defining the route, use `req_obj_type` kwarg to specify the type.
from doctor import create_routes, put, Route

create_routes((
    Route('/foo/', methods=[
        put(update_foo, req_obj_type=FooObject)]
    )
))

This allows you to simply send the following json body:

{
  "name": "a name",
  "id": 1
}

Without specifying a value for req_obj_type when defining the route, you would have to send a foo key in your json body for it to validate and properly send the request data to your logic function:

{
  "foo": {
    "name": "a name",
    "id": 1
  }
}

Running Code Before or After the Logic Function

Sometimes you may want to run some code before the logic function gets called to perform some logging or inspect the request. Likwise you may also want run some extra code after a logic function returns its results but before an HTTP response is returned.

You can optionally do either of these actions by passing a callable when defining a Route to either the before or after kwargs.

Note

The callable passed to the after kwarg must accept a single parameter which is the result of your logic function.

import logging

def log_before_logic():
    logging.debug('Before logic gets called')


def log_after_logic(result):
    logging.debug('After logic function result is %s', result)


def logic():
    return "result"
from doctor import create_routes, get, Route

create_routes((
    Route('/foo/', methods=[
        get(logic)],
        before=log_before_logic,
        after=log_after_logic
    )
))

Adding Response Headers

If you need more control over the response, your logic function can return a Response instance. For example if you would like to have your logic function force download a csv file you could do the following:

from doctor.response import Response

def download_csv():
    data = '1,2,3\n4,5,6\n'
    return Response(data, {
        'Content-Type': 'text/csv',
        'Content-Disposition': 'attachment; filename=data.csv',
    })

The Response class takes the response data as the first parameter and a dict of HTTP response headers as the second parameter. The response headers can contain standard and any custom values.

Response Validation

doctor can also validate your responses.

Enabling

By default doctor will only raise exceptions for invalid response when there is a truthy value for the environment variable RAISE_RESPONSE_VALIDATION_ERRORS. This will cause a HTTP 400 error which wil give details on why the response is not valid. If either of those conditions are not true only a warning will be logged.

Usage

To tell doctor to validate a response you must define a return annotation on your logic function. Simply use doctor types to define a valid response and annotate it on your logic function.

from doctor import types

Color = types.enum('A color', enum=['blue', 'green'])
Colors = types.array('Array of colors', items=Color)

def get_colors() -> Colors:
    # ... logic to fetch colors
    return colors

The return value of get_colors will be validated against the type that we created. If we have enabled raising response validation errors and our response does not validate, we will get a 400 response. If the above example returned an array with an integer like [1] our response would look like:

` Response to GET /colors `[1]` does not validate: {0: 'Must be a valid choice.'} `

Example API Documentation

This API documentation is generated using the autoflask Sphinx directive. See the section on Generating API Documentation for more information.

API Status

Retrieve

GET /
Logic Func:

status()

Request Headers:
 

Example Request:

curl http://127.0.0.1:8080/ -X GET -H 'Authorization: testtoken'

Example Response:

"Notes API v1.0.0"

Notes (v1)

Create

POST /note/

Create a new note.

Logic Func:

create_note()

Request JSON Object:
 
  • body (str) – Required. Note body
  • done (bool) – Marks if a note is done or not. (Defaults to False)
Request Headers:
 
Response JSON Object:
 
  • body (str) – Note body
  • done (bool) – Marks if a note is done or not.
  • note_id (int) – Note ID

Example Request:

curl http://127.0.0.1:8080/note/ -X POST -H 'Authorization: testtoken' \
  -H 'Content-Type: application/json' -d \
  '{
    "body": "body",
    "done": false
   }'

Example Response:

{
  "body": "body",
  "done": false,
  "note_id": 2
}

Delete

DELETE /note/(int: note_id)/

Delete an existing note.

Logic Func:

delete_note()

Query Parameters:
 
  • note_id (int) – Required. Note ID
Request Headers:
 

Example Request:

curl http://127.0.0.1:8080/note/1/ -X DELETE -H 'Authorization: testtoken'

Example Response:


Retrieve

GET /note/(int: note_id)/

Get a note by ID.

Logic Func:

get_note()

Query Parameters:
 
  • note_id (int) – Required. Note ID
  • note_type (str) – Required. The type of note Must be one of: [‘quick’, ‘detailed’].
Request Headers:
 
Response JSON Object:
 
  • body (str) – Note body
  • done (bool) – Marks if a note is done or not.
  • note_id (int) – Note ID

Example Request:

curl 'http://127.0.0.1:8080/note/1/?note_type=quick' -X GET \
  -H 'Authorization: testtoken'

Example Response:

{
  "body": "Example body",
  "done": true,
  "note_id": 1
}

Retrieve List

GET /note/

Get a list of notes.

Logic Func:

get_notes()

Request Headers:
 
  • Authorization – The auth token for the authenticated user.
  • X-GeoIp-Country – An ISO 3166-1 alpha-2 country code.
Response JSON Array of Objects:
 
  • body (str) – Note body
  • done (bool) – Marks if a note is done or not.
  • note_id (int) – Note ID

Example Request:

curl http://127.0.0.1:8080/note/ -X GET -H 'Authorization: testtoken' \
  -H 'X-GeoIp-Country: US'

Example Response:

[
  {
    "body": "Example body",
    "done": true,
    "note_id": 1
  }
]

Update

PUT /note/(int: note_id)/

Update an existing note.

Logic Func:

update_note()

Request JSON Object:
 
  • note_id (int) – Required. Note ID
  • body (str) – Note body (Defaults to None)
  • done (bool) – Marks if a note is done or not. (Defaults to None)
Request Headers:
 
Response JSON Object:
 
  • body (str) – Note body
  • done (bool) – Marks if a note is done or not.
  • note_id (int) – Note ID

Example Request:

curl http://127.0.0.1:8080/note/1/ -X PUT -H 'Authorization: testtoken' \
  -H 'Content-Type: application/json' -d \
  '{
    "body": "body",
    "done": false,
    "note_id": 1
   }'

Example Response:

{
  "body": "body",
  "done": false,
  "note_id": 1
}

Flask Module Documentation

exception doctor.flask.HTTP400Exception(description=None, errors=None)[source]

Represents a HTTP 400 error.

Parameters:
  • description (Optional[str]) – The error description.
  • errors (Optional[dict]) – A dict containing all validation errors during the request. The key is the param name and the value is the error message.
exception doctor.flask.HTTP401Exception(description=None, errors=None)[source]
exception doctor.flask.HTTP403Exception(description=None, errors=None)[source]
exception doctor.flask.HTTP404Exception(description=None, errors=None)[source]
exception doctor.flask.HTTP409Exception(description=None, errors=None)[source]
exception doctor.flask.HTTP500Exception(description=None, errors=None)[source]
exception doctor.flask.SchematicHTTPException(description=None, errors=None)[source]

Schematic specific sub-class of werkzeug’s BadRequest.

Note that this adds a flask-restful specific data attribute to the class, as the error wouldn’t render properly without it.

Parameters:
  • description (Optional[str]) – The error description.
  • errors (Optional[dict]) – A dict containing all validation errors during the request. The key is the param name and the value is the error message.
doctor.flask.create_routes(routes)[source]

A thin wrapper around create_routes that passes in flask specific values.

Parameters:routes (Tuple[Route]) – A tuple containing the route and another tuple with all http methods allowed for the route.
Return type:List[Tuple[str, Resource]]
Returns:A list of tuples containing the route and generated handler.
doctor.flask.handle_http(handler, args, kwargs, logic)[source]

Handle a Flask HTTP request

Parameters:
  • handler (Resource) – flask_restful.Resource: An instance of a Flask Restful resource class.
  • args (tuple) – Any positional arguments passed to the wrapper method.
  • kwargs (dict) – Any keyword arguments passed to the wrapper method.
  • logic (callable) – The callable to invoke to actually perform the business logic for this request.
doctor.flask.should_raise_response_validation_errors()[source]

Returns if the library should raise response validation errors or not.

If the environment variable RAISE_RESPONSE_VALIDATION_ERRORS is set, it will return True.

Return type:bool
Returns:True if it should, False otherwise.