# 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] - [PyYAML][pypi-yaml] ## 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 YAML document (anything except object keys) with custom plugin invocations. These plugins look up and return your dynamic content.
Input Output
```yaml --- api: "0.13" space: My Hackerspace # This is a plugin invocation # with no arguments state: !space_state {} sensors: # This is a plugin invocation with # arguments. They are passed to the # plugin function as kwargs. network_connections: !network_connections networks: [ "2.4 GHz", "5 GHz" ] ``` ```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" } ] } } ```
## 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.yaml`. This file controls all the internal settings of the server. - The **response template** file, located at `/etc/spaceapi-server/template.yaml`. 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.yaml`. The following options are currently available: ```yaml --- # The address to listen on. address: "::1" # The TCP port to listen on. port: 8000 # The Bottle backend server to use. server: wsgiref # Path to the SpaceAPI response template file. template: template.yaml # Path to the directory containing your plugins. plugins_dir: 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.yaml`. 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" (apart from the conversion from YAML to JSON). 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 # The plugin can be invoked by using the !space_state YAML tag 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 a constructor in PyYAML's parser with the function's name as tag (e.g. `!space_state). 2. Call the template function in your template: ```yaml # ... state: !space_state # ... ``` 3. Configure the server: ```yaml # ... template: template.yaml 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.yaml ``` ### 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.` 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 for use as a PyYAML constructor, 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 the function's name as a YAML tag in the parser. The decorated function may take arguments (always passed as `**kwargs`, so `*args`, or arguments before `*` won't work) that can be represented in a YAML file. 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 [] ``` ```yaml # ... state: !lookup_sensor query: SELECT timestamp, value FROM people_now_present LIMIT 1 # ... ``` [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-yaml]: https://pypi.org/project/PyYAML/ [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