spaceapi-server/README.md
2019-12-02 22:27:30 +01:00

350 lines
8.8 KiB
Markdown

# SpaceAPI Server
[![pipeline status](https://gitlab.com/s3lph/spaceapi-server/badges/master/pipeline.svg)][master]
[![coverage report](https://gitlab.com/s3lph/spaceapi-server/badges/master/coverage.svg)][master]
A lightweight server for [SpaceAPI][spaceapi] endpoints. Includes
support for pluggable templating, so dynamic content, like sensor
values, can be added.
## Dependencies
- Python 3 (>=3.6)
- [Bottle][pypi-bottle]
- [Jinja2][pypi-jinja2]
## License
[MIT License][mit]
## 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][jinja], from which you can
invoke custom plugins, which look up and return your dynamic content.
<table>
<thead>
<tr>
<td>Input</td>
<td>Output</td>
</tr>
</thead>
<tbody>
<tr>
<td>
```json
{
"api": "0.13",
"space": "My Hackerspace",
"state": "{{ space_state() }}",
"sensors": {
"network_connections": "{{ network_connections() }}"
}
}
```
</td>
<td>
```json
{
"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"
}
]
}
}
```
</td>
</tr>
</tbody>
</table>
## Usage
### 0. Download
Head over to the [Releases][releases], download and install the
package that suits your needs. Alternatively, clone the repo and get
started.
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:
```json
{
"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][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:
```python
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][jinja].
2. Call the template function in your template:
```json
{
# ...
"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:
```json
# ...
"template": "template.json",
"plugins_dir": "plugins",
"plugins": {
"filestate": {
"filename": "/var/space_state"
}
}
# ...
```
### 5. Start the Server
Start the server with e.g.:
```bash
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:
```bash
python3 -m spaceapi_server /path/to/config.json
```
### 6. Test the Server
```bash
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:
```python
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:
```python
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 []
```
```json
{
# ...
"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:
```python
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 []
```
```json
{
# ...
"state": "{{ 'SELECT timestamp, value FROM people_now_present LIMIT 1' | lookup_sensor }}"
# ...
}
```
[master]: https://gitlab.com/s3lph/spaceapi-server/commits/master
[releases]: https://gitlab.com/s3lph/spaceapi-server/-/releases
[spaceapi]: https://spaceapi.io/
[pypi-bottle]: https://pypi.org/project/bottle/
[pypi-jinja2]: https://pypi.org/project/Jinja2/
[mit]: https://gitlab.com/s3lph/spaceapi-server/blob/master/LICENSE
[spaceapi-getting-started]: https://spaceapi.io/getting-started/
[jinja]: https://jinja.palletsprojects.com/