diff --git a/README.md b/README.md index e69de29..f0d553f 100644 --- a/README.md +++ b/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/ \ No newline at end of file diff --git a/examples/plugins/example.py b/examples/plugins/example.py index 67989b6..a0de9cf 100644 --- a/examples/plugins/example.py +++ b/examples/plugins/example.py @@ -4,33 +4,33 @@ from spaceapi_server import config, plugins @plugins.template_function def example_function(name: str): - ''' + """ This function is registered as a Jinja2 function. It can be used like this: {{ example_function('the Spanish Inquisition') }} - ''' + """ return f'Nobody expects {name}' @plugins.template_filter def example_filter(name: str): - ''' + """ This function is registered as a Jinja2 filter. It can be used like this: {{ 'the Spanish Inquisition' | example_filter }} - ''' + """ return f'Nobody expects {name}' @plugins.template_test def example_test(name: str): - ''' + """ This function is registered as a Jinja2 test. It can be used like this: {% if 'the Spanish Inquisition' is example_test %} - ''' + """ # Config lookup example. A plugin's config should be below # `.plugins[plugin_name]` (JSONPath) # 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 # back to a default value test_value = conf.get('test_value', 'the Spanish Inquisition') diff --git a/setup.py b/setup.py index 61e3a7d..a005663 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ setup( keywords='spaceapi', url='https://gitlab.com/s3lph/spaceapi-server', 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', install_requires=[ 'bottle', diff --git a/spaceapi_server/config/__init__.py b/spaceapi_server/config/__init__.py index 9b35c46..f9e9156 100644 --- a/spaceapi_server/config/__init__.py +++ b/spaceapi_server/config/__init__.py @@ -9,7 +9,7 @@ __CONFIG = {} def load(filename: str) -> None: """ Load a JSON-formatted configuration file. - :type filename: The config file to load. + :param filename: The config file to load. """ global __CONFIG # Open and parse the JSON config file @@ -23,3 +23,12 @@ def get() -> dict: """ global __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, {}) diff --git a/spaceapi_server/plugins/__init__.py b/spaceapi_server/plugins/__init__.py index 7677a9d..a593eab 100644 --- a/spaceapi_server/plugins/__init__.py +++ b/spaceapi_server/plugins/__init__.py @@ -5,7 +5,7 @@ from spaceapi_server.template import env_init def template_function(fn): """ 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 env = env_init() @@ -17,7 +17,7 @@ def template_function(fn): def template_filter(fn): """ 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 env = env_init() @@ -29,7 +29,7 @@ def template_filter(fn): def template_test(fn): """ 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 env = env_init() diff --git a/spaceapi_server/server/__init__.py b/spaceapi_server/server/__init__.py index d4639c5..36193da 100644 --- a/spaceapi_server/server/__init__.py +++ b/spaceapi_server/server/__init__.py @@ -17,7 +17,7 @@ def render_traverse(obj): """ Walk through a complex, JSON-serializable data structure, and pass string objects through the Jinja2 templating engine. - :type obj: The object to traverse. + :param obj: The object to traverse. """ if isinstance(obj, list): # list -> recurse into each item diff --git a/spaceapi_server/template/__init__.py b/spaceapi_server/template/__init__.py index 945db25..59b7663 100644 --- a/spaceapi_server/template/__init__.py +++ b/spaceapi_server/template/__init__.py @@ -10,7 +10,7 @@ _ENV = None def env_init(force: bool = False): """ Initialize the Jinja2 environment. - :type force: If true, force reload the environment. + :param force: If true, force reload the environment. """ global _ENV if _ENV is None or force: @@ -22,7 +22,7 @@ def env_init(force: bool = False): def render(template: str): """ 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 env = env_init()