353 lines
No EOL
9 KiB
Markdown
353 lines
No EOL
9 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. There also is a Container Image available through the
|
|
[Gitlab registry][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:
|
|
|
|
```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/
|
|
[registry]: https://gitlab.com/s3lph/spaceapi-server/container_registry |