A lightweight SpaceAPI (spaceapi.io) endpoint server
Find a file
2020-06-21 05:13:15 +02:00
examples Remove template_filter decorator, as there really isn't any use case for it. 2019-11-30 04:49:01 +01:00
package release.py: Replace default user agent by that of curl 2020-06-21 05:13:15 +02:00
spaceapi_server Add a CI step to create a container image 2020-06-20 03:43:08 +02:00
.gitignore Initial commit 2019-11-25 02:48:12 +01:00
.gitlab-ci.yml Revert "Fix CI script" 2020-06-20 04:29:06 +02:00
CHANGELOG.md Add a CI step to create a container image 2020-06-20 03:43:08 +02:00
LICENSE Add MIT License 2019-11-25 02:53:18 +01:00
README.md Update Readme 2020-06-20 04:39:50 +02:00
requirements.txt Initial commit 2019-11-25 02:48:12 +01:00
setup.cfg Initial commit 2019-11-25 02:48:12 +01:00
setup.py Add a CI step to create a container image 2020-06-20 03:43:08 +02:00

SpaceAPI Server

pipeline status coverage report

A lightweight server for SpaceAPI endpoints. Includes support for pluggable templating, so dynamic content, like sensor values, can be added.

Dependencies

License

MIT License

Introduction

This project is an attempt to implement a lightweight, yet versatile SpaceAPI endpoint server. In its simplest configuration, it just serves a plain, static, boring JSON document.

In order to provide dynamic content (e.g. whether your space is currently open), you can replace parts of the JSON document (anything except object keys) with Jinja2 templates, from which you can invoke custom plugins, which look up and return your dynamic content.

Input Output
{
  "api": "0.13",
  "space": "My Hackerspace",
  "state": "{{ space_state() }}",
  "sensors": {
    "network_connections": "{{ network_connections() }}"
  }
}
{
  "api": "0.13",
  "state": {
    "open": true,
    "lastchange": 1575160777,
    "message": "Visitors Welcome!"
  },
  "sensors": {
    "network_connections": [
      {
        "value": 4,
        "type": "wifi",
        "name": "2.4 GHz"
      },
      {
        "value": 7,
        "type": "wifi",
        "name": "5 GHz"
      }
    ]
  }
}

Usage

0. Download

Head over to the Releases, download and install the package that suits your needs. Alternatively, clone the repo and get started. There also is a Container Image available through the Gitlab registry tagged as registry.gitlab.com/s3lph/spaceapi-server.

The remainder of this document assumes that you installed the server as an OS distribution package.

1. Overview

The configuration of this server consists of three parts:

  • The main configuration file, usually located at /etc/spaceapi-server/config.json. This file controls all the internal settings of the server.
  • The response template file, located at /etc/spaceapi-server/template.json. This file defines the content served by your sever.
  • The plugins directory, located at /etc/spaceapi-server/plugins/. Here you can put your plugins for rendering dynamic content.

2. Configure the Server

Open the file /etc/spaceapi-server/config.json.

The following options are currently available:

{
    "address": "::1",     # The address to listen on.
    "port": 8000,         # The TCP port to listen on.
    "server": "wsgiref",  # The Bottle backend server to use.
    "template": "template.json",  # Path to the SpaceAPI response template file.
    "plugins_dir": "plugins",     # Path to the directory containing your plugins.

    "plugins": {
        # Plugin-specific configuration should go in here, separated by plugin
        "my_plugin": {
            "my_option": "Hello, World!",
            "my_other_option": [ 42, 1337 ]
        }
    }
}

3. Configure a Static SpaceAPI Endpoint

Open the file /etc/spaceapi-server/template.json. By default it contains a minimal example response. If you only want to serve static content, your template.json should simply contain the SpaceAPI JSON response you want to serve.

The content is served "almost-as-is" (once parsed and re-serialized).

To learn about how a SpaceAPI response should look like, have a look at SpaceAPI: Getting Started.

4. Add Dynamic Content

This example guides you through adding a dynamic state property to your SpaceAPI endpoint. We'll use the following (rather simple, and probably not too useful) data source: Check a certain file, and mark the space as open depending on its existence.

  1. Create a plugin to fetch the data. Let's name it filestate.py and put in in our plugins directory:

    import os
    from spaceapi_server import config, plugins
    
    @plugins.template_function
    def space_state():
        # Get the plugin config dict
        conf = config.get_plugin_config('filestate')
        # Get the filename
        filename = conf.get('filename', '/var/space_state')
        try:
            # Get the file's properties
            stat = os.stat(filename)
        except FileNotFoundError:
            # File doesn't exist, aka. space is closed
            return {
                'open': False
            }
        # File exists, aka. space is open.  Also report the mtime as "last changed" timestamp
        return {
            'open': True,
            'lastchange': int(stat.st_mtime)
        }
    

    The @template_function decorator registers the function as a callable in Jinja's globals. There's also @template_filter, which registers a Jinja2 filter. For more information on the Jinja2 templating engine, see Jinja2.

  2. Call the template function in your template:

    {
        # ...
        "state": "{{ space_state() }}"
        # ...
    }
    

    Although the value for the state key is a string containing the Jinja2 template, the return value of your template function is a complex data type, which will be inserted into the result as such. Please be aware that only the first token of a templated string is used in the result. This limitation is caused by the way Jinja2 is (ab-) used.

  3. Configure the server:

    # ...
    "template": "template.json",
    "plugins_dir": "plugins",
    "plugins": {
        "filestate": {
            "filename": "/var/space_state"
        }
    }
    # ...
    

5. Start the Server

Start the server with e.g.:

systemctl start spaceapi-server.service

To reload the configuration, template and plugins, send a SIGHUP, e.g. through systemctl reload.

If you need to run the server ad-hoc, you can start it with:

python3 -m spaceapi_server /path/to/config.json

6. Test the Server

curl http://localhost:8000/

You should be greeted with the SpaceAPI endpoint response.

Plugin API Reference

Configuration

The following functions provide access to values defined in the configuration file.

spaceapi_server.config.get_plugin_config(name: str)

This function returns a plugin's configuration.

The function takes one argument, the name of the plugin. This name is used to look up the plugin configuration.

The function returns the content present at the key .plugins.<name> of the global configuration file, or an empty object if absent.

Usage:

from spaceapi_server import plugins

print(plugins.get_plugin_config('my_plugin'))

Templating

The following decorators register a function in the Jinja2 environment, so they become usable from within templates. They are invoked when the template is rendered, which happens for each HTTP request.

If performance is an issue, consider applying caching, either in your plugins, or by using a caching HTTP reverse proxy.

spaceapi_server.plugins.template_function

This decorator registers a function as a global function in the Jinja2 environment.

The decorated function may take any arguments that can be represented in a Jinja2 template.

The decorated function may return any value that can be serialized into JSON. This includes objects and arrays.

Usage:

from spaceapi_server import plugins

@plugins.template_function
def lookup_sensor(query, default=None):
    # Do something with the query
    result = ...
    # If the loo
    if not result:
        return default or []
{
  # ...
  "state": "{{ lookup_sensor('SELECT timestamp, value FROM people_now_present LIMIT 1') }}"
  # ...
}

spaceapi_server.plugins.template_filter

This decorator registers a function as a filter in the Jinja2 environment. The decorated function must take at least one argument.

The decorated function may take any arguments that can be represented in a Jinja2 template.

The decorated function may return any value that can be serialized into JSON. This includes objects and arrays.

Usage:

from spaceapi_server import plugins

@plugins.template_filter
def lookup_sensor(query, default=None):
    # Do something with the query
    result = ...
    # If the loo
    if not result:
        return default or []
{
  # ...
  "state": "{{ 'SELECT timestamp, value FROM people_now_present LIMIT 1' | lookup_sensor }}"
  # ...
}