Replace Jinja2 with PyYAML

This commit is contained in:
s3lph 2021-05-30 17:52:25 +02:00
parent a3eb452786
commit 093a40fefa
31 changed files with 197 additions and 376 deletions

View file

@ -1,5 +1,5 @@
--- ---
image: s3lph/spaceapi-server-ci:20200620-01 image: s3lph/spaceapi-server-ci:20210530-01
stages: stages:
- test - test
@ -57,7 +57,7 @@ build_debian:
Section: web Section: web
Priority: optional Priority: optional
Architecture: all Architecture: all
Depends: python3 (>= 3.6), python3-jinja2, python3-bottle Depends: python3 (>= 3.9), python3-yaml, python3-bottle
Description: Lightweight SpaceAPI endpoint server Description: Lightweight SpaceAPI endpoint server
Lightweight server for SpaceAPI endpoints. Includes support for pluggable Lightweight server for SpaceAPI endpoints. Includes support for pluggable
templating, so dynamic content, like sensor values, can be added. templating, so dynamic content, like sensor values, can be added.
@ -77,8 +77,8 @@ build_debian:
- python3 setup.py egg_info install --root=package/debian/spaceapi-server/ --prefix=/usr --optimize=1 - python3 setup.py egg_info install --root=package/debian/spaceapi-server/ --prefix=/usr --optimize=1
- cd package/debian/spaceapi-server - cd package/debian/spaceapi-server
- mkdir -p usr/lib/python3/dist-packages/ - mkdir -p usr/lib/python3/dist-packages/
- cp -r usr/lib/python3.8/site-packages/spaceapi_server* usr/lib/python3/dist-packages/ - cp -r usr/lib/python3.9/site-packages/spaceapi_server* usr/lib/python3/dist-packages/
- rm -rf usr/lib/python3.8/ - rm -rf usr/lib/python3.9/
# Remove compiled Python files # Remove compiled Python files
- find usr/lib/python3/dist-packages -name __pycache__ -exec rm -r {} \; 2>/dev/null || true - find usr/lib/python3/dist-packages -name __pycache__ -exec rm -r {} \; 2>/dev/null || true
- find usr/lib/python3/dist-packages -name '*.pyc' -exec rm {} \; - find usr/lib/python3/dist-packages -name '*.pyc' -exec rm {} \;
@ -109,7 +109,7 @@ build_archlinux:
script: script:
- find package/archlinux -name .gitkeep -delete - find package/archlinux -name .gitkeep -delete
# Install dependencies # Install dependencies
- pacman -Sy --noconfirm namcap python python-setuptools python-pip python-wheel python-jinja python-bottle base-devel - pacman -Sy --noconfirm namcap python python-setuptools python-pip python-wheel python-yaml python-bottle base-devel
- export SPACEAPI_SERVER_VERSION=$(python -c 'import spaceapi_server; print(spaceapi_server.__version__)') - export SPACEAPI_SERVER_VERSION=$(python -c 'import spaceapi_server; print(spaceapi_server.__version__)')
# Copy example plugin # Copy example plugin
- install -m0644 examples/plugins/example.py package/archlinux/spaceapi-server/etc/spaceapi-server/plugins/example.py - install -m0644 examples/plugins/example.py package/archlinux/spaceapi-server/etc/spaceapi-server/plugins/example.py

View file

@ -1,5 +1,19 @@
# SpaceAPI Server Changelog # SpaceAPI Server Changelog
<!-- BEGIN RELEASE v0.4 -->
## Version 0.4
Replace Jinja2+JSON with PyYAML
### Changes
<!-- BEGIN CHANGES 0.4 -->
- Switch config file format from JSON to YAML
- Remove Jinja2, replaced with YAML custom tags for plugin calls
<!-- END CHANGES 0.4 -->
<!-- END RELEASE v0.4 -->
<!-- BEGIN RELEASE v0.3.1 --> <!-- BEGIN RELEASE v0.3.1 -->
## Version 0.3 ## Version 0.3

167
README.md
View file

@ -11,7 +11,7 @@ values, can be added.
- Python 3 (>=3.6) - Python 3 (>=3.6)
- [Bottle][pypi-bottle] - [Bottle][pypi-bottle]
- [Jinja2][pypi-jinja2] - [PyYAML][pypi-yaml]
## License ## License
@ -24,9 +24,9 @@ SpaceAPI endpoint server. In its simplest configuration, it just
serves a plain, static, boring JSON document. serves a plain, static, boring JSON document.
In order to provide dynamic content (e.g. whether your space is In order to provide dynamic content (e.g. whether your space is
currently open), you can replace parts of the JSON document (anything currently open), you can replace parts of the YAML document (anything
except object keys) with [Jinja2 templates][jinja], from which you can except object keys) with custom plugin invocations. These plugins
invoke custom plugins, which look up and return your dynamic content. look up and return your dynamic content.
<table> <table>
<thead> <thead>
@ -39,15 +39,19 @@ invoke custom plugins, which look up and return your dynamic content.
<tr> <tr>
<td> <td>
```json ```yaml
{ ---
"api": "0.13", api: "0.13"
"space": "My Hackerspace", space: My Hackerspace
"state": "{{ space_state() }}", # This is a plugin invocation
"sensors": { # with no arguments
"network_connections": "{{ network_connections() }}" 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" ]
``` ```
</td> </td>
@ -100,10 +104,10 @@ server as an OS distribution package.
The configuration of this server consists of three parts: The configuration of this server consists of three parts:
- The **main configuration** file, usually located at - The **main configuration** file, usually located at
`/etc/spaceapi-server/config.json`. This file controls all the `/etc/spaceapi-server/config.yaml`. This file controls all the
internal settings of the server. internal settings of the server.
- The **response template** file, located at - The **response template** file, located at
`/etc/spaceapi-server/template.json`. This file defines the content `/etc/spaceapi-server/template.yaml`. This file defines the content
served by your sever. served by your sever.
- The **plugins** directory, located at - The **plugins** directory, located at
`/etc/spaceapi-server/plugins/`. Here you can put your plugins for `/etc/spaceapi-server/plugins/`. Here you can put your plugins for
@ -111,36 +115,38 @@ The configuration of this server consists of three parts:
### 2. Configure the Server ### 2. Configure the Server
Open the file `/etc/spaceapi-server/config.json`. Open the file `/etc/spaceapi-server/config.yaml`.
The following options are currently available: The following options are currently available:
```json ```yaml
{ ---
"address": "::1", # The address to listen on. # The address to listen on.
"port": 8000, # The TCP port to listen on. address: "::1"
"server": "wsgiref", # The Bottle backend server to use. # The TCP port to listen on.
"template": "template.json", # Path to the SpaceAPI response template file. port: 8000
"plugins_dir": "plugins", # Path to the directory containing your plugins. # The Bottle backend server to use.
server: wsgiref
"plugins": { # 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 # Plugin-specific configuration should go in here, separated by plugin
"my_plugin": { my_plugin:
"my_option": "Hello, World!", my_option: "Hello, World!"
"my_other_option": [ 42, 1337 ] my_other_option: [ 42, 1337 ]
}
}
}
``` ```
### 3. Configure a Static SpaceAPI Endpoint ### 3. Configure a Static SpaceAPI Endpoint
Open the file `/etc/spaceapi-server/template.json`. By default it Open the file `/etc/spaceapi-server/template.yaml`. By default it
contains a minimal example response. If you only want to serve static contains a minimal example response. If you only want to serve static
content, your `template.json` should simply contain the SpaceAPI JSON content, your `template.json` should simply contain the SpaceAPI JSON
response you want to serve. response you want to serve.
The content is served "almost-as-is" (once parsed and re-serialized). 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 To learn about how a SpaceAPI response should look like, have a look
at [SpaceAPI: Getting Started][spaceapi-getting-started]. at [SpaceAPI: Getting Started][spaceapi-getting-started].
@ -160,6 +166,7 @@ the space as open depending on its existence.
from spaceapi_server import config, plugins from spaceapi_server import config, plugins
@plugins.template_function @plugins.template_function
# The plugin can be invoked by using the !space_state YAML tag
def space_state(): def space_state():
# Get the plugin config dict # Get the plugin config dict
conf = config.get_plugin_config('filestate') conf = config.get_plugin_config('filestate')
@ -180,39 +187,27 @@ the space as open depending on its existence.
} }
``` ```
The `@template_function` decorator registers the function as a The `@template_function` decorator registers a constructor in
callable in Jinja's globals. There's also `@template_filter`, PyYAML's parser with the function's name as tag
which registers a Jinja2 filter. For more information on the (e.g. `!space_state).
Jinja2 templating engine, see [Jinja2][jinja].
2. Call the template function in your template: 2. Call the template function in your template:
```json ```yaml
{
# ... # ...
"state": "{{ space_state() }}" 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: 3. Configure the server:
```json ```yaml
# ... # ...
"template": "template.json", template: template.yaml
"plugins_dir": "plugins", plugins_dir: plugins
"plugins": { plugins:
"filestate": { filestate:
"filename": "/var/space_state" filename: /var/space_state
}
}
# ... # ...
``` ```
@ -230,7 +225,7 @@ e.g. through `systemctl reload`.
If you need to run the server ad-hoc, you can start it with: If you need to run the server ad-hoc, you can start it with:
```bash ```bash
python3 -m spaceapi_server /path/to/config.json python3 -m spaceapi_server /path/to/config.yaml
``` ```
### 6. Test the Server ### 6. Test the Server
@ -267,8 +262,8 @@ print(plugins.get_plugin_config('my_plugin'))
### Templating ### Templating
The following decorators register a function in the Jinja2 The following decorators register a function for use as a PyYAML
environment, so they become usable from within templates. They are constructor, so they become usable from within templates. They are
invoked when the template is rendered, which happens for each HTTP invoked when the template is rendered, which happens for each HTTP
request. request.
@ -277,11 +272,11 @@ plugins, or by using a caching HTTP reverse proxy.
#### `spaceapi_server.plugins.template_function` #### `spaceapi_server.plugins.template_function`
This decorator registers a function as a **global function** in the This decorator registers the function's name as a YAML tag in the parser.
Jinja2 environment.
The decorated function may take any arguments that can be represented The decorated function may take arguments (always passed as
in a Jinja2 template. `**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 The decorated function may return any value that can be serialized
into JSON. This includes objects and arrays. into JSON. This includes objects and arrays.
@ -300,45 +295,11 @@ def lookup_sensor(query, default=None):
return default or [] return default or []
``` ```
```json ```yaml
{ # ...
# ... state: !lookup_sensor
"state": "{{ lookup_sensor('SELECT timestamp, value FROM people_now_present LIMIT 1') }}" query: 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 }}"
# ...
}
``` ```
@ -346,7 +307,7 @@ def lookup_sensor(query, default=None):
[releases]: https://gitlab.com/s3lph/spaceapi-server/-/releases [releases]: https://gitlab.com/s3lph/spaceapi-server/-/releases
[spaceapi]: https://spaceapi.io/ [spaceapi]: https://spaceapi.io/
[pypi-bottle]: https://pypi.org/project/bottle/ [pypi-bottle]: https://pypi.org/project/bottle/
[pypi-jinja2]: https://pypi.org/project/Jinja2/ [pypi-yaml]: https://pypi.org/project/PyYAML/
[mit]: https://gitlab.com/s3lph/spaceapi-server/blob/master/LICENSE [mit]: https://gitlab.com/s3lph/spaceapi-server/blob/master/LICENSE
[spaceapi-getting-started]: https://spaceapi.io/getting-started/ [spaceapi-getting-started]: https://spaceapi.io/getting-started/
[jinja]: https://jinja.palletsprojects.com/ [jinja]: https://jinja.palletsprojects.com/

View file

@ -1,4 +1,4 @@
FROM python:3.8-buster as python FROM python:3.9-buster as python
RUN apt update \ RUN apt update \
&& apt install -y --no-install-recommends lintian rsync sudo docker.io \ && apt install -y --no-install-recommends lintian rsync sudo docker.io \

View file

@ -8,7 +8,7 @@ url="https://gitlab.com/s3lph/spaceapi-server"
license=('MIT') license=('MIT')
groups=() groups=()
depends=('python' depends=('python'
'python-jinja' 'python-yaml'
'python-bottle') 'python-bottle')
makedepends=('python-setuptools') makedepends=('python-setuptools')
checkdepends=() checkdepends=()
@ -16,8 +16,8 @@ optdepends=()
provides=() provides=()
conflicts=() conflicts=()
replaces=() replaces=()
backup=('etc/spaceapi-server/config.json' backup=('etc/spaceapi-server/config.yaml'
'etc/spaceapi-server/template.json' 'etc/spaceapi-server/template.yaml'
'etc/spaceapi-server/plugins') 'etc/spaceapi-server/plugins')
install=$pkgname.install install=$pkgname.install
changelog=$pkgname.changelog changelog=$pkgname.changelog

View file

@ -1,7 +0,0 @@
{
"address": "::",
"port": 8080,
"template": "/etc/spaceapi-server/template.json",
"plugins_dir": "/etc/spaceapi-server/plugins",
"plugins": {}
}

View file

@ -0,0 +1,6 @@
---
address: "::"
port: 8080
template: /etc/spaceapi-server/template.json
plugins_dir: /etc/spaceapi-server/plugins
plugins: {}

View file

@ -1,19 +0,0 @@
{
"api": "0.13",
"space": "Example Space",
"logo": "https://example.org/logo.png",
"url": "https://example.org",
"location": {
"lat": 0.0,
"lon": 0.0
},
"state": {
"open": null
},
"contact": {
"email": "example@example.org"
},
"issue_report_channels": [
"email"
]
}

View file

@ -0,0 +1,14 @@
---
api: "0.13"
space: Example
logo: https://example.org/logo.png
url: https://example.org
location:
lat: 0.0
lon: 0.0
state:
open: null
contact:
email: example@example.org
issue_report_channels:
- email

View file

@ -2,7 +2,7 @@
Description=Lightweight SpaceAPI Endpoint Server Description=Lightweight SpaceAPI Endpoint Server
[Service] [Service]
ExecStart=/usr/bin/python -m spaceapi_server /etc/spaceapi-server/config.json ExecStart=/usr/bin/python -m spaceapi_server /etc/spaceapi-server/config.yaml
ExecReload=/usr/bin/kill -HUP $MAINPID ExecReload=/usr/bin/kill -HUP $MAINPID
User=spaceapi-server User=spaceapi-server

View file

@ -1,3 +1,3 @@
/etc/spaceapi-server/config.json /etc/spaceapi-server/config.yaml
/etc/spaceapi-server/template.json /etc/spaceapi-server/template.yaml
/etc/spaceapi-server/plugins/example.py /etc/spaceapi-server/plugins/example.py

View file

@ -1,7 +0,0 @@
{
"address": "::",
"port": 8080,
"template": "/etc/spaceapi-server/template.json",
"plugins_dir": "/etc/spaceapi-server/plugins",
"plugins": {}
}

View file

@ -0,0 +1,6 @@
---
address: "::"
port: 8080
template: /etc/spaceapi-server/template.json
plugins_dir: /etc/spaceapi-server/plugins
plugins: {}

View file

@ -1,19 +0,0 @@
{
"api": "0.13",
"space": "Example Space",
"logo": "https://example.org/logo.png",
"url": "https://example.org",
"location": {
"lat": 0.0,
"lon": 0.0
},
"state": {
"open": null
},
"contact": {
"email": "example@example.org"
},
"issue_report_channels": [
"email"
]
}

View file

@ -0,0 +1,14 @@
---
api: "0.13"
space: Example
logo: https://example.org/logo.png
url: https://example.org
location:
lat: 0.0
lon: 0.0
state:
open: null
contact:
email: example@example.org
issue_report_channels:
- email

View file

@ -2,7 +2,7 @@
Description=Lightweight SpaceAPI Endpoint Server Description=Lightweight SpaceAPI Endpoint Server
[Service] [Service]
ExecStart=/usr/bin/python3 -m spaceapi_server /etc/spaceapi-server/config.json ExecStart=/usr/bin/python3 -m spaceapi_server /etc/spaceapi-server/config.yaml
ExecReload=/usr/bin/kill -HUP $MAINPID ExecReload=/usr/bin/kill -HUP $MAINPID
User=spaceapi-server User=spaceapi-server

View file

@ -1,13 +1,13 @@
FROM python:3.8-alpine FROM python:3.9-alpine
ADD spaceapi_server/ /spaceapi_server/ ADD spaceapi_server/ /spaceapi_server/
ADD setup.py /setup.py ADD setup.py /setup.py
RUN python setup.py install && mkdir -p /config/plugins && chmod 0755 -R /config RUN python setup.py install && mkdir -p /config/plugins && chmod 0755 -R /config
ADD package/docker/config.json /config/config.json ADD package/docker/config.yaml /config/config.yaml
ADD package/docker/template.json /config/template.json ADD package/docker/template.yaml /config/template.yaml
VOLUME /config VOLUME /config
EXPOSE 8000/tcp EXPOSE 8000/tcp
USER 1000 USER 1000
ENTRYPOINT [ "/usr/local/bin/python3.8", "-m", "spaceapi_server", "/config/config.json" ] ENTRYPOINT [ "/usr/local/bin/python3.9", "-m", "spaceapi_server", "/config/config.yaml" ]

View file

@ -1,8 +0,0 @@
{
"address": "0.0.0.0",
"port": 8000,
"server": "wsgiref",
"template": "/config/template.json",
"plugins_dir": "/config/plugins",
"plugins": {}
}

View file

@ -0,0 +1,6 @@
---
address: "::"
port: 8080
template: /etc/spaceapi-server/template.json
plugins_dir: /etc/spaceapi-server/plugins
plugins: {}

View file

@ -1,23 +0,0 @@
{
"api": "0.13",
"space": "example.space",
"logo": "http://example.space/logo.png",
"url": "http://example.space",
"location": {
"address": "example.space, 42 Example Street, 1337 Example Town",
"lon": 42.42,
"lat": 13.37
},
"contact": {
"email": "example@example.space"
},
"issue_report_channels": [
"email"
],
"state": {
"open": null
},
"projects": [
"http://example.space/projects"
]
}

View file

@ -0,0 +1,14 @@
---
api: "0.13"
space: Example
logo: https://example.org/logo.png
url: https://example.org
location:
lat: 0.0
lon: 0.0
state:
open: null
contact:
email: example@example.org
issue_report_channels:
- email

View file

@ -19,7 +19,7 @@ setup(
python_requires='>=3.6', python_requires='>=3.6',
install_requires=[ install_requires=[
'bottle', 'bottle',
'jinja2==2.10' 'PyYAML',
], ],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
@ -31,7 +31,7 @@ setup(
'Environment :: Web Environment', 'Environment :: Web Environment',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.9',
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application' 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application'
] ]
) )

View file

@ -1,2 +1,2 @@
__version__ = '0.3.1' __version__ = '0.4'

View file

@ -1,5 +1,5 @@
import json import yaml
# The parsed config object # The parsed config object
@ -8,13 +8,13 @@ __CONFIG = {}
def load(filename: str) -> None: def load(filename: str) -> None:
""" """
Load a JSON-formatted configuration file. Load a YAML-formatted configuration file.
:param 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 YAML config file
with open(filename, 'r') as conf: with open(filename, 'r') as conf:
__CONFIG = json.load(conf) __CONFIG = yaml.safe_load(conf)
def get() -> dict: def get() -> dict:

View file

@ -1,38 +0,0 @@
import unittest
import tempfile
from spaceapi_server.config import load, get
from spaceapi_server.plugins import get_plugin_config
class ConfigTest(unittest.TestCase):
CONFIG = '''
{
"foo": "bar",
"baz": 42,
"plugins": {
"myplugin": {
"foo": "baz"
}
}
}
'''
def setUp(self) -> None:
_, self.temp = tempfile.mkstemp('.json', 'spaceapi_server_config_', text=True)
with open(self.temp, 'w') as f:
f.write(self.CONFIG)
def test_load(self):
pre = get()
pre_plugin = get_plugin_config('myplugin')
self.assertEqual({}, pre)
self.assertEqual({}, pre_plugin)
load(self.temp)
post = get()
post_plugin = get_plugin_config('myplugin')
self.assertEqual('bar', post['foo'])
self.assertEqual(42, post['baz'])
self.assertEqual('baz', post_plugin['foo'])

View file

@ -1,4 +1,6 @@
import yaml
from spaceapi_server import config, template from spaceapi_server import config, template
@ -7,22 +9,8 @@ def template_function(fn):
Register the decorated function as a callable template function. Register the decorated function as a callable template function.
:param fn: The function to register. :param fn: The function to register.
""" """
# Make sure the Jinja2 environment is initialized yaml.SafeLoader.add_constructor(f'!{fn.__name__}', template.plugin_constructor)
env = template._env_init() template.PluginInvocation.register_plugin(fn.__name__, fn)
# Add the function to the environment's globals
env.globals[fn.__name__] = fn
return fn
def template_filter(fn):
"""
Register the decorated function as a template filter.
:param fn: The function to register.
"""
# Make sure the Jinja2 environment is initialized
env = template._env_init()
# Add the function to the environment's filters
env.filters[fn.__name__] = fn
return fn return fn

View file

@ -1,6 +1,7 @@
import os import os
import sys import sys
import json import json
import yaml
import signal import signal
import importlib import importlib
@ -51,10 +52,10 @@ def load(*args, **kwargs):
m_spec.loader.exec_module(module) m_spec.loader.exec_module(module)
# Get the template path # Get the template path
template_path = conf.get('template', 'template.json') template_path = conf.get('template', 'template.yaml')
# Load and parse the JSON template # Load and parse the JSON template
with open(template_path, 'r') as f: with open(template_path, 'r') as f:
__TEMPLATE = json.load(f) __TEMPLATE = yaml.safe_load(f)
def init(): def init():

View file

@ -1,44 +1,31 @@
import json
import jinja2 class PluginInvocation():
_plugins = {}
def __init__(self, name, data):
super().__init__()
if name not in self._plugins:
raise KeyError(f'No such plugin function: {name}')
self._name = name
self._data = data
def __call__(self):
return self._plugins[self._name](**self._data)
@classmethod
def register_plugin(cls, name, fn):
cls._plugins[name] = fn
# The Jinja2 environment def plugin_constructor(loader, node):
__ENV = None data = loader.construct_mapping(node)
return PluginInvocation(node.tag[1:], data)
def _env_init(force: bool = False):
"""
Initialize the Jinja2 environment.
:param force: If true, force reload the environment.
"""
global __ENV
if __ENV is None or force:
# Use json.dumps as finalizer in order to preserve complex data structures
__ENV = jinja2.Environment(finalize=json.dumps)
return __ENV
def render(template: str):
"""
Render the given string as a Jinja2 template.
:param template: The template string to render.
"""
# Make sure the Jinja2 environment is initialized
env = _env_init()
# Create a Jinja2 template from the input string
t = env.from_string(template)
decoder = json.JSONDecoder()
# Render the template and turn the JSON dump back into complex data structures
# Only parse the first JSON object in the string, ignore the rest
obj, i = decoder.raw_decode(t.render())
return obj
def render_traverse(obj): def render_traverse(obj):
""" """
Walk through a complex, JSON-serializable data structure, and pass Walk through a complex, JSON-serializable data structure, and
string objects through the Jinja2 templating engine. invoke plugins if for custom tags.
:param obj: The object to traverse. :param obj: The object to traverse.
""" """
if isinstance(obj, list): if isinstance(obj, list):
@ -47,9 +34,9 @@ def render_traverse(obj):
elif isinstance(obj, dict): elif isinstance(obj, dict):
# dict -> recurse into the value of each (key, value) # dict -> recurse into the value of each (key, value)
return {k: render_traverse(v) for k, v in obj.items()} return {k: render_traverse(v) for k, v in obj.items()}
elif isinstance(obj, str): elif isinstance(obj, PluginInvocation):
# str -> template # PluginTag -> invoke the plugin with the stored arguments
return render(obj) return obj()
else: else:
# anything else -> return as-is # anything else -> return as-is
return obj return obj

View file

@ -1,69 +0,0 @@
import unittest
from spaceapi_server.template import _env_init, render, render_traverse
from spaceapi_server.plugins import template_function, template_filter
class TemplateTest(unittest.TestCase):
@staticmethod
@template_function
def template_test_function(value):
return f'test_{value}'
@staticmethod
@template_filter
def template_test_filter(value, other):
return f'test_{other}_{value}'
@staticmethod
@template_function
def template_test_function_nocache():
TemplateTest.counter1 += 1
return TemplateTest.counter1
def setUp(self) -> None:
TemplateTest.counter1 = 0
def test_template(self):
env = _env_init()
self.assertEqual('"not a template"', env.from_string('not a template').render())
self.assertEqual('"a template"', env.from_string('{{ "a template" }}').render())
self.assertEqual('42', env.from_string('{{ 42 }}').render())
self.assertEqual('["foo"]', env.from_string('{{ [ "foo" ] }}').render())
self.assertEqual('{"foo": ["bar"]}', env.from_string('{{ { "foo": [ "bar" ] } }}').render())
def test_render(self):
self.assertEqual('not a template', render('not a template'))
self.assertEqual('a template', render('{{ "a template" }}'))
self.assertEqual(42, render('{{ 42 }}'))
self.assertEqual(['foo'], render('{{ [ "foo" ] }}'))
self.assertEqual({'foo': ['bar']}, render('{{ { "foo": [ "bar" ] } }}'))
self.assertEqual('foo', render('foo{{ "bar" }}'))
def test_render_traverse(self):
template = {
'foo': 42,
'bar': [1, 2, 3],
'notemplate': 'foo',
'builtin': '{{ [ 1337, 42 ] | first }}',
'test_functions': {
'test_function': '{{ template_test_function("foo") }}',
'test_filter': '{{ "bar" | template_test_filter("other") }}'
}
}
rendered = render_traverse(template)
self.assertEqual(42, rendered['foo'])
self.assertEqual([1, 2, 3], rendered['bar'])
self.assertEqual('foo', rendered['notemplate'])
self.assertEqual(1337, rendered['builtin'])
self.assertEqual('test_foo', rendered['test_functions']['test_function'])
self.assertEqual('test_other_bar', rendered['test_functions']['test_filter'])
def test_no_cache(self):
template = ['{{ template_test_function_nocache() }}']
r1 = render_traverse(template)
r2 = render_traverse(template)
self.assertEqual(1, r1[0])
self.assertEqual(2, r2[0])