Add README, fix docstrings
This commit is contained in:
parent
8d71a0b457
commit
839d8e25cb
7 changed files with 143 additions and 15 deletions
118
README.md
118
README.md
|
@ -0,0 +1,118 @@
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m spaceapi_server config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```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, e.g. `.plugins.sqlite.database`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Serve a Static SpaceAPI Endpoint
|
||||||
|
|
||||||
|
Have a look at [SpaceAPI: Getting Started][spaceapi-getting-started]. If you only want to serve static content, your
|
||||||
|
`template.json` may consist of regular JSON data, which is served almost-as-is (once parsed and re-serialized).
|
||||||
|
|
||||||
|
## 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 `mybackend.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('mybackend')
|
||||||
|
# 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, and `@template_test`, which registers a test. 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.
|
||||||
|
|
||||||
|
3. Configure the server:
|
||||||
|
|
||||||
|
```json
|
||||||
|
# ...
|
||||||
|
"template": "template.json",
|
||||||
|
"plugins_dir": "plugins",
|
||||||
|
"plugins": {
|
||||||
|
"mybackend": {
|
||||||
|
"filename", "/var/space_state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Start the server and query it.
|
||||||
|
|
||||||
|
[master]: https://gitlab.com/s3lph/spaceapi-server/commits/master
|
||||||
|
[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/
|
|
@ -4,33 +4,33 @@ from spaceapi_server import config, plugins
|
||||||
|
|
||||||
@plugins.template_function
|
@plugins.template_function
|
||||||
def example_function(name: str):
|
def example_function(name: str):
|
||||||
'''
|
"""
|
||||||
This function is registered as a Jinja2 function. It can be used like this:
|
This function is registered as a Jinja2 function. It can be used like this:
|
||||||
{{ example_function('the Spanish Inquisition') }}
|
{{ example_function('the Spanish Inquisition') }}
|
||||||
'''
|
"""
|
||||||
return f'Nobody expects {name}'
|
return f'Nobody expects {name}'
|
||||||
|
|
||||||
|
|
||||||
@plugins.template_filter
|
@plugins.template_filter
|
||||||
def example_filter(name: str):
|
def example_filter(name: str):
|
||||||
'''
|
"""
|
||||||
This function is registered as a Jinja2 filter. It can be used like this:
|
This function is registered as a Jinja2 filter. It can be used like this:
|
||||||
{{ 'the Spanish Inquisition' | example_filter }}
|
{{ 'the Spanish Inquisition' | example_filter }}
|
||||||
'''
|
"""
|
||||||
return f'Nobody expects {name}'
|
return f'Nobody expects {name}'
|
||||||
|
|
||||||
|
|
||||||
@plugins.template_test
|
@plugins.template_test
|
||||||
def example_test(name: str):
|
def example_test(name: str):
|
||||||
'''
|
"""
|
||||||
This function is registered as a Jinja2 test. It can be used like this:
|
This function is registered as a Jinja2 test. It can be used like this:
|
||||||
{% if 'the Spanish Inquisition' is example_test %}
|
{% if 'the Spanish Inquisition' is example_test %}
|
||||||
'''
|
"""
|
||||||
# Config lookup example. A plugin's config should be below
|
# Config lookup example. A plugin's config should be below
|
||||||
# `.plugins[plugin_name]` (JSONPath)
|
# `.plugins[plugin_name]` (JSONPath)
|
||||||
|
|
||||||
# Get the .plugins.example dict
|
# Get the .plugins.example dict
|
||||||
conf = config.get().get('plugins', {}).get('example', {})
|
conf = config.get_plugin_config('example')
|
||||||
# Get the .test_value property from the plugin config, falling
|
# Get the .test_value property from the plugin config, falling
|
||||||
# back to a default value
|
# back to a default value
|
||||||
test_value = conf.get('test_value', 'the Spanish Inquisition')
|
test_value = conf.get('test_value', 'the Spanish Inquisition')
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -14,7 +14,8 @@ setup(
|
||||||
keywords='spaceapi',
|
keywords='spaceapi',
|
||||||
url='https://gitlab.com/s3lph/spaceapi-server',
|
url='https://gitlab.com/s3lph/spaceapi-server',
|
||||||
packages=find_packages(exclude=['*.test']),
|
packages=find_packages(exclude=['*.test']),
|
||||||
long_description='A lightweight server for SpaceAPI endpoints. Includes support for pluggable templating, so dynamic content, like sensor values, can be added.',
|
long_description='A lightweight server for SpaceAPI endpoints. Includes support for pluggable templating, '
|
||||||
|
'so dynamic content, like sensor values, can be added.',
|
||||||
python_requires='>=3.6',
|
python_requires='>=3.6',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'bottle',
|
'bottle',
|
||||||
|
|
|
@ -9,7 +9,7 @@ __CONFIG = {}
|
||||||
def load(filename: str) -> None:
|
def load(filename: str) -> None:
|
||||||
"""
|
"""
|
||||||
Load a JSON-formatted configuration file.
|
Load a JSON-formatted configuration file.
|
||||||
:type filename: The config file to load.
|
:param filename: The config file to load.
|
||||||
"""
|
"""
|
||||||
global __CONFIG
|
global __CONFIG
|
||||||
# Open and parse the JSON config file
|
# Open and parse the JSON config file
|
||||||
|
@ -23,3 +23,12 @@ def get() -> dict:
|
||||||
"""
|
"""
|
||||||
global __CONFIG
|
global __CONFIG
|
||||||
return __CONFIG
|
return __CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_config(name: str):
|
||||||
|
"""
|
||||||
|
Return a plugin's configuration under .plugins[name]
|
||||||
|
:param name: The plugin name.
|
||||||
|
"""
|
||||||
|
global __CONFIG
|
||||||
|
return __CONFIG.get('plugins', {}).get(name, {})
|
||||||
|
|
|
@ -5,7 +5,7 @@ from spaceapi_server.template import env_init
|
||||||
def template_function(fn):
|
def template_function(fn):
|
||||||
"""
|
"""
|
||||||
Register the decorated function as a callable template function.
|
Register the decorated function as a callable template function.
|
||||||
:type fn: The function to register.
|
:param fn: The function to register.
|
||||||
"""
|
"""
|
||||||
# Make sure the Jinja2 environment is initialized
|
# Make sure the Jinja2 environment is initialized
|
||||||
env = env_init()
|
env = env_init()
|
||||||
|
@ -17,7 +17,7 @@ def template_function(fn):
|
||||||
def template_filter(fn):
|
def template_filter(fn):
|
||||||
"""
|
"""
|
||||||
Register the decorated function as a template filter.
|
Register the decorated function as a template filter.
|
||||||
:type fn: The function to register.
|
:param fn: The function to register.
|
||||||
"""
|
"""
|
||||||
# Make sure the Jinja2 environment is initialized
|
# Make sure the Jinja2 environment is initialized
|
||||||
env = env_init()
|
env = env_init()
|
||||||
|
@ -29,7 +29,7 @@ def template_filter(fn):
|
||||||
def template_test(fn):
|
def template_test(fn):
|
||||||
"""
|
"""
|
||||||
Register the decorated function as a template test.
|
Register the decorated function as a template test.
|
||||||
:type fn: The function to register.
|
:param fn: The function to register.
|
||||||
"""
|
"""
|
||||||
# Make sure the Jinja2 environment is initialized
|
# Make sure the Jinja2 environment is initialized
|
||||||
env = env_init()
|
env = env_init()
|
||||||
|
|
|
@ -17,7 +17,7 @@ def render_traverse(obj):
|
||||||
"""
|
"""
|
||||||
Walk through a complex, JSON-serializable data structure, and pass
|
Walk through a complex, JSON-serializable data structure, and pass
|
||||||
string objects through the Jinja2 templating engine.
|
string objects through the Jinja2 templating engine.
|
||||||
:type obj: The object to traverse.
|
:param obj: The object to traverse.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, list):
|
if isinstance(obj, list):
|
||||||
# list -> recurse into each item
|
# list -> recurse into each item
|
||||||
|
|
|
@ -10,7 +10,7 @@ _ENV = None
|
||||||
def env_init(force: bool = False):
|
def env_init(force: bool = False):
|
||||||
"""
|
"""
|
||||||
Initialize the Jinja2 environment.
|
Initialize the Jinja2 environment.
|
||||||
:type force: If true, force reload the environment.
|
:param force: If true, force reload the environment.
|
||||||
"""
|
"""
|
||||||
global _ENV
|
global _ENV
|
||||||
if _ENV is None or force:
|
if _ENV is None or force:
|
||||||
|
@ -22,7 +22,7 @@ def env_init(force: bool = False):
|
||||||
def render(template: str):
|
def render(template: str):
|
||||||
"""
|
"""
|
||||||
Render the given string as a Jinja2 template.
|
Render the given string as a Jinja2 template.
|
||||||
:type template: The template string to render.
|
:param template: The template string to render.
|
||||||
"""
|
"""
|
||||||
# Make sure the Jinaj2 environment is initialized
|
# Make sure the Jinaj2 environment is initialized
|
||||||
env = env_init()
|
env = env_init()
|
||||||
|
|
Loading…
Reference in a new issue