Development:Core API Framework

From OpenLP
Jump to: navigation, search

As of OpenLP 2.8, we have moved the API into the OpenLP core, and written an API framework which plugins can use to provide methods within the API. Plugins should provide RESTful endpoints.

Internally, the Core API Framework borrows heavily from the Flask framework, but is written from scratch. It uses the Waitress WSGI server and WebOb for requests and responses internally, and thus depends on those two packages although they both only depend on the Python standard library.

Endpoints

Each plugin should create a single endpoint, which will be the base URL for all methods within the plugin. Creating an endpoint is as simple as creating an instance of the Endpoint class.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

Once you've created your endpoint, and added some routes (more on that below), you want to register your endpoint with the Core API Framework so that it knows about it when running the API. To do this, you need to pass your endpoint object to the register_endpoint function.

from openlp.core.api import Endpoint, register_endpoint

songs_endpoint = Endpoint('songs')

...  # code goes here

register_endpoint(songs_endpoint)

Routes

From within each endpoint, multiple routes can be created. The route decorator is used to provide routes to the methods within your endpoint.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('')
def query(request):
    """
    Return all the songs.
    """
    # This URL looks like: "/songs"
    pass

Arguments

If you want to pass arguments within your URLs, simply put their names in curly braces.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('{song_id}')
def get(request, song_id):
    """
    Return a specific song.
    """
    # This URL looks like: "/songs/abc123"
    pass

You can also use regular expressions to narrow down the exact format of the argument.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('{song_id:\d+}')
def get(request, song_id):
    """
    Return a specific song.
    """
    # This URL looks like: "/songs/123456"
    pass

Multiple arguments can be specified, and they don't even need to be separated by slashes.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('by-date/{year:\d\d\d\d}-{month:\d\d}-{day:\d\d}')
def by_date(request, year, month, day):
    """
    Return a specific song.
    """
    # This URL looks like: "/songs/by-date/2016-06-19"
    pass

HTTP Methods

By default, the route decorator will only respond to GETs. To make your method respond to another HTTP verb (such as POST, PATCH, PUT, HEAD, DELETE, etc), use the method parameter in the decorator.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('', method='POST')
def create(request):
    """
    Create a new song.
    """
    pass

@songs_endpoint.route('{song_id:\d+}', method='PUT')
def update(request, song_id):
    """
    Update an existing song.
    """
    pass

Requests

Each route that is called is passed a request parameter as the first parameter to the function. This is a Request object from WebOb.

Using the request object, you can extract URL parameters, the body, form data, or even JSON.

URL parameters

Should you wish to use ye olde parameters in your URLs, use the request.GET dictionary to get them.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('')
def query(request):
    """
    Return all the songs.
    """
    # Let's say the URL looks like: "/songs?title=Holy"
    if request.GET.get('title'):
        # request.GET is basically a dictionary
        title = request.GET['title']
    ...

Form data

Form data, also known as POST parameters, can be retrieved through the request.POST dictionary.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('', method='POST')
def create(request):
    """
    Create a new song
    """
    song = Song()
    if request.POST.get('title'):
        song.title = request.POST['title']
    if request.POST.get('lyrics'):
        song.lyrics = request.POST['lyrics']
    ...

JSON data

Since this is an API, most of the data you'll be interacting with will be in the form of a JSON object. Use the json attribute on the request object, which will be a Python dictionary of the JSON in the body.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('', method='POST')
def create(request):
    """
    Create a new song
    """
    song = Song()
    song_dict = request.json
    song.title = song_dict['title']
    ...

Request body

Of course if you just want raw access to the body of the request, use the body attribute on the request object.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('', method='POST')
def create(request):
    """
    Create a new song
    """
    log.debug(request.body)
    ...

Responses

To return your result to the browser, simply use the return statement and provide your output.

from openlp.core.api import Endpoint

hello_endpoint = Endpoint('hello')

@hello_endpoint.route('')
def hello(request):
    """
    Say hello world
    """
    return 'Hello world!'

String output will be sent back as HTML (content type "text/html"), and dictionaries will be converted into JSON and sent back as such (content type "application/json").

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')

@songs_endpoint.route('')
def query(request):
    """
    Return all the songs.
    """
    songs = db.query(Song).fetch()
    # Automatically returns the songs as JSON
    return {'songs': [dictify(song) for song in songs]}

To alter the return code you're sending, return a tuple of (response, status) instead.

from openlp.core.api import Endpoint

teapot_endpoint = Endpoint('teapot')

@teapot_endpoint.route('')
def teapot(request):
    """
    Send back a different status
    """
    return 'I\'m a teapot', 418

To send back additional HTTP headers, add a dictionary of headers as a third item to the tuple.

from openlp.core.api import Endpoint

redirect_endpoint = Endpoint('redirect')

@redirect_endpoint.route('')
def redirect(request):
    """
    Send the browser to the OpenLP website
    """
    return 'Moved Permanently', 301, {'Location': 'https://openlp.org/'}

Note: The headers are added after the content type is set, so if you wish to change the content type, you can do so by adding it to the headers.

from openlp.core.api import Endpoint

hello_endpoint = Endpoint('hello')

@hello_endpoint.route('')
def hello(request):
    """
    Say hello world
    """
    return 'document.write("Hello world!");', 200, {'Content-Type': 'text/javascript'}

RESTful APIs

REST, or REpresentational State Transfer, is a way to allow requesting systems to access and manipulate textual representations of Web resources using a uniform and predefined set of stateless operations. In terms of the web, RESTful APIs use the standard GET, POST, PUT, PATCH and DELETE methods for performing CRUD operations using a URL with HTTP response codes to indicate the response type.

Rather than repeating what someone else has written, go and read these quick tips on RESTful APIs.

In most cases, the OpenLP API won't be providing create, update or delete methods for the endpoints. However, GETs will be used extensively. Let's look at an example (based on the rest of this document) for how to implement a search method and a get method for the Songs plugin.

from openlp.core.api import Endpoint

songs_endpoint = Endpoint('songs')


@songs_endpoint.route('')
def query(request):
    """
    Return a (filtered) list of songs.
    """
    query_params = []
    if request.GET.get('title'):
        query_params.append(Song.search_title.like('%{}%'.format(clean_string(request.GET['title']))))
    if request.GET.get('lyrics'):
        query_params.append(Song.search_lyrics.like('%{}%'.format(clean_string(request.GET['lyrics']))))
    songs = db.get_all_objects(Song, _or(*query_params))
    return {'songs': [dictify(song) for song in songs]}


@songs_endpoint.route('{song_id}')
def get(request, song_id):
    """
    Return a song
    """
    song = db.get_object(Song, song_id)
    if not song:
        return 'Not found', 404
    return {'songs': [dictify(song)]}

Templates

The Core API Framework comes with Mako templates built in. The Endpoint class takes a template_dir parameter in the constructor, and a render_template method provides a way to render your template. Passing keyword parameters into the render_template method will inject those parameters into the template as variables.

import os
from openlp.core import get_version
from openlp.core.api import Endpoint

template_dir = os.path.join(os.path.dirname(__file__), 'templates')
stage_endpoint = Endpoint('stage', template_dir=template_dir)

@stage_endpoint.route('')
def index(request):
    """
    Show the stage view.
    """
    openlp_version = get_version()
    return stage_endpoint.render_template('index.mako', version=openlp_version)

Static files

No web framework would be complete without a way to serve static files. The Endpoint class takes a static_dir in the constructor. All files served off /[endpoint]/static are served directly from the static directory.

Furthermore, the base URL of the static directory is automatically added as a variable in your template. Simply use ${static_url} in your template so that you never have to remember what it is.

WARNING: Raoul might not have implemented this yet. If it doesn't work, moan at him till it is. If it does work, moan at him to remove this notice.
import os
from openlp.core.api import Endpoint

static_dir = os.path.join(os.path.dirname(__file__), 'static')
stage_endpoint = Endpoint('stage', static_dir=static_dir)

@stage_endpoint.route('')
def index(request):
    """
    Show the stage view.
    """
    return stage_endpoint.render_template('index.mako')
<html>
  <head>
    <link rel="stylesheet" href="${static_url}/stage.css">
  </head>
  <body>
  ...
  </body>
</html>