From 093a40fefa662ec15b3ffd3e5d14c0fcbffaa39d Mon Sep 17 00:00:00 2001 From: s3lph Date: Sun, 30 May 2021 17:52:25 +0200 Subject: [PATCH] Replace Jinja2 with PyYAML --- .gitlab-ci.yml | 10 +- CHANGELOG.md | 14 ++ README.md | 175 +++++++----------- package/Dockerfile | 2 +- package/archlinux/PKGBUILD | 6 +- .../etc/spaceapi-server/config.json | 7 - .../etc/spaceapi-server/config.yaml | 6 + .../etc/spaceapi-server/template.json | 19 -- .../etc/spaceapi-server/template.yaml | 14 ++ .../systemd/system/spaceapi-server.service | 2 +- .../debian/spaceapi-server/DEBIAN/conffiles | 4 +- .../etc/spaceapi-server/config.json | 7 - .../etc/spaceapi-server/config.yaml | 6 + .../etc/spaceapi-server/template.json | 19 -- .../etc/spaceapi-server/template.yaml | 14 ++ .../systemd/system/spaceapi-server.service | 2 +- package/docker/Dockerfile | 8 +- package/docker/config.json | 8 - package/docker/config.yaml | 6 + package/docker/template.json | 23 --- package/docker/template.yaml | 14 ++ setup.py | 4 +- spaceapi_server/__init__.py | 2 +- spaceapi_server/config/__init__.py | 8 +- spaceapi_server/config/test/__init__.py | 0 spaceapi_server/config/test/test_config.py | 38 ---- spaceapi_server/plugins/__init__.py | 20 +- spaceapi_server/server/__init__.py | 5 +- spaceapi_server/template/__init__.py | 61 +++--- spaceapi_server/template/test/__init__.py | 0 .../template/test/test_template.py | 69 ------- 31 files changed, 197 insertions(+), 376 deletions(-) delete mode 100644 package/archlinux/spaceapi-server/etc/spaceapi-server/config.json create mode 100644 package/archlinux/spaceapi-server/etc/spaceapi-server/config.yaml delete mode 100644 package/archlinux/spaceapi-server/etc/spaceapi-server/template.json create mode 100644 package/archlinux/spaceapi-server/etc/spaceapi-server/template.yaml delete mode 100644 package/debian/spaceapi-server/etc/spaceapi-server/config.json create mode 100644 package/debian/spaceapi-server/etc/spaceapi-server/config.yaml delete mode 100644 package/debian/spaceapi-server/etc/spaceapi-server/template.json create mode 100644 package/debian/spaceapi-server/etc/spaceapi-server/template.yaml delete mode 100644 package/docker/config.json create mode 100644 package/docker/config.yaml delete mode 100644 package/docker/template.json create mode 100644 package/docker/template.yaml delete mode 100644 spaceapi_server/config/test/__init__.py delete mode 100644 spaceapi_server/config/test/test_config.py delete mode 100644 spaceapi_server/template/test/__init__.py delete mode 100644 spaceapi_server/template/test/test_template.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ca4fed2..e196fb2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ --- -image: s3lph/spaceapi-server-ci:20200620-01 +image: s3lph/spaceapi-server-ci:20210530-01 stages: - test @@ -57,7 +57,7 @@ build_debian: Section: web Priority: optional Architecture: all - Depends: python3 (>= 3.6), python3-jinja2, python3-bottle + Depends: python3 (>= 3.9), python3-yaml, python3-bottle Description: Lightweight SpaceAPI endpoint server Lightweight server for SpaceAPI endpoints. Includes support for pluggable 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 - cd package/debian/spaceapi-server - mkdir -p usr/lib/python3/dist-packages/ - - cp -r usr/lib/python3.8/site-packages/spaceapi_server* usr/lib/python3/dist-packages/ - - rm -rf usr/lib/python3.8/ + - cp -r usr/lib/python3.9/site-packages/spaceapi_server* usr/lib/python3/dist-packages/ + - rm -rf usr/lib/python3.9/ # 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 '*.pyc' -exec rm {} \; @@ -109,7 +109,7 @@ build_archlinux: script: - find package/archlinux -name .gitkeep -delete # 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__)') # Copy example plugin - install -m0644 examples/plugins/example.py package/archlinux/spaceapi-server/etc/spaceapi-server/plugins/example.py diff --git a/CHANGELOG.md b/CHANGELOG.md index da53629..0bb2c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # SpaceAPI Server Changelog + +## Version 0.4 + +Replace Jinja2+JSON with PyYAML + +### Changes + + +- Switch config file format from JSON to YAML +- Remove Jinja2, replaced with YAML custom tags for plugin calls + + + + ## Version 0.3 diff --git a/README.md b/README.md index 67aee4f..623fde0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ values, can be added. - Python 3 (>=3.6) - [Bottle][pypi-bottle] -- [Jinja2][pypi-jinja2] +- [PyYAML][pypi-yaml] ## License @@ -24,9 +24,9 @@ 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. +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. @@ -39,15 +39,19 @@ invoke custom plugins, which look up and return your dynamic content. @@ -100,10 +104,10 @@ server as an OS distribution package. 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 + `/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.json`. This file defines the content + `/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 @@ -111,36 +115,38 @@ The configuration of this server consists of three parts: ### 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: -```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 ] - } - } -} +```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.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 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). +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]. @@ -160,6 +166,7 @@ the space as open depending on its existence. 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') @@ -180,39 +187,27 @@ the space as open depending on its existence. } ``` - 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]. + 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: - ```json - { - # ... - "state": "{{ space_state() }}" - # ... - } + ```yaml + # ... + 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 + ```yaml # ... - "template": "template.json", - "plugins_dir": "plugins", - "plugins": { - "filestate": { - "filename": "/var/space_state" - } - } + template: template.yaml + plugins_dir: plugins + plugins: + filestate: + 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: ```bash -python3 -m spaceapi_server /path/to/config.json +python3 -m spaceapi_server /path/to/config.yaml ``` ### 6. Test the Server @@ -267,8 +262,8 @@ 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 +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. @@ -277,11 +272,11 @@ 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. +This decorator registers the function's name as a YAML tag in the parser. -The decorated function may take any arguments that can be represented -in a Jinja2 template. +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. @@ -300,45 +295,11 @@ def lookup_sensor(query, default=None): 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 }}" - # ... -} +```yaml +# ... +state: !lookup_sensor + query: SELECT timestamp, value FROM people_now_present LIMIT 1 +# ... ``` @@ -346,8 +307,8 @@ def lookup_sensor(query, default=None): [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/ +[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 \ No newline at end of file +[registry]: https://gitlab.com/s3lph/spaceapi-server/container_registry diff --git a/package/Dockerfile b/package/Dockerfile index 0feb8f5..8ab0293 100644 --- a/package/Dockerfile +++ b/package/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-buster as python +FROM python:3.9-buster as python RUN apt update \ && apt install -y --no-install-recommends lintian rsync sudo docker.io \ diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index a7d699f..06a9a02 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -8,7 +8,7 @@ url="https://gitlab.com/s3lph/spaceapi-server" license=('MIT') groups=() depends=('python' - 'python-jinja' + 'python-yaml' 'python-bottle') makedepends=('python-setuptools') checkdepends=() @@ -16,8 +16,8 @@ optdepends=() provides=() conflicts=() replaces=() -backup=('etc/spaceapi-server/config.json' - 'etc/spaceapi-server/template.json' +backup=('etc/spaceapi-server/config.yaml' + 'etc/spaceapi-server/template.yaml' 'etc/spaceapi-server/plugins') install=$pkgname.install changelog=$pkgname.changelog diff --git a/package/archlinux/spaceapi-server/etc/spaceapi-server/config.json b/package/archlinux/spaceapi-server/etc/spaceapi-server/config.json deleted file mode 100644 index bb5b41a..0000000 --- a/package/archlinux/spaceapi-server/etc/spaceapi-server/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "address": "::", - "port": 8080, - "template": "/etc/spaceapi-server/template.json", - "plugins_dir": "/etc/spaceapi-server/plugins", - "plugins": {} -} \ No newline at end of file diff --git a/package/archlinux/spaceapi-server/etc/spaceapi-server/config.yaml b/package/archlinux/spaceapi-server/etc/spaceapi-server/config.yaml new file mode 100644 index 0000000..25033a3 --- /dev/null +++ b/package/archlinux/spaceapi-server/etc/spaceapi-server/config.yaml @@ -0,0 +1,6 @@ +--- +address: "::" +port: 8080 +template: /etc/spaceapi-server/template.json +plugins_dir: /etc/spaceapi-server/plugins +plugins: {} diff --git a/package/archlinux/spaceapi-server/etc/spaceapi-server/template.json b/package/archlinux/spaceapi-server/etc/spaceapi-server/template.json deleted file mode 100644 index 2af901c..0000000 --- a/package/archlinux/spaceapi-server/etc/spaceapi-server/template.json +++ /dev/null @@ -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" - ] -} \ No newline at end of file diff --git a/package/archlinux/spaceapi-server/etc/spaceapi-server/template.yaml b/package/archlinux/spaceapi-server/etc/spaceapi-server/template.yaml new file mode 100644 index 0000000..d331863 --- /dev/null +++ b/package/archlinux/spaceapi-server/etc/spaceapi-server/template.yaml @@ -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 diff --git a/package/archlinux/spaceapi-server/usr/lib/systemd/system/spaceapi-server.service b/package/archlinux/spaceapi-server/usr/lib/systemd/system/spaceapi-server.service index 3c2b9b7..4a43ef2 100644 --- a/package/archlinux/spaceapi-server/usr/lib/systemd/system/spaceapi-server.service +++ b/package/archlinux/spaceapi-server/usr/lib/systemd/system/spaceapi-server.service @@ -2,7 +2,7 @@ Description=Lightweight SpaceAPI Endpoint Server [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 User=spaceapi-server diff --git a/package/debian/spaceapi-server/DEBIAN/conffiles b/package/debian/spaceapi-server/DEBIAN/conffiles index 9f42ddb..a070eb5 100644 --- a/package/debian/spaceapi-server/DEBIAN/conffiles +++ b/package/debian/spaceapi-server/DEBIAN/conffiles @@ -1,3 +1,3 @@ -/etc/spaceapi-server/config.json -/etc/spaceapi-server/template.json +/etc/spaceapi-server/config.yaml +/etc/spaceapi-server/template.yaml /etc/spaceapi-server/plugins/example.py diff --git a/package/debian/spaceapi-server/etc/spaceapi-server/config.json b/package/debian/spaceapi-server/etc/spaceapi-server/config.json deleted file mode 100644 index bb5b41a..0000000 --- a/package/debian/spaceapi-server/etc/spaceapi-server/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "address": "::", - "port": 8080, - "template": "/etc/spaceapi-server/template.json", - "plugins_dir": "/etc/spaceapi-server/plugins", - "plugins": {} -} \ No newline at end of file diff --git a/package/debian/spaceapi-server/etc/spaceapi-server/config.yaml b/package/debian/spaceapi-server/etc/spaceapi-server/config.yaml new file mode 100644 index 0000000..25033a3 --- /dev/null +++ b/package/debian/spaceapi-server/etc/spaceapi-server/config.yaml @@ -0,0 +1,6 @@ +--- +address: "::" +port: 8080 +template: /etc/spaceapi-server/template.json +plugins_dir: /etc/spaceapi-server/plugins +plugins: {} diff --git a/package/debian/spaceapi-server/etc/spaceapi-server/template.json b/package/debian/spaceapi-server/etc/spaceapi-server/template.json deleted file mode 100644 index 2af901c..0000000 --- a/package/debian/spaceapi-server/etc/spaceapi-server/template.json +++ /dev/null @@ -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" - ] -} \ No newline at end of file diff --git a/package/debian/spaceapi-server/etc/spaceapi-server/template.yaml b/package/debian/spaceapi-server/etc/spaceapi-server/template.yaml new file mode 100644 index 0000000..d331863 --- /dev/null +++ b/package/debian/spaceapi-server/etc/spaceapi-server/template.yaml @@ -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 diff --git a/package/debian/spaceapi-server/lib/systemd/system/spaceapi-server.service b/package/debian/spaceapi-server/lib/systemd/system/spaceapi-server.service index 1d0d84b..a07f873 100644 --- a/package/debian/spaceapi-server/lib/systemd/system/spaceapi-server.service +++ b/package/debian/spaceapi-server/lib/systemd/system/spaceapi-server.service @@ -2,7 +2,7 @@ Description=Lightweight SpaceAPI Endpoint Server [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 User=spaceapi-server diff --git a/package/docker/Dockerfile b/package/docker/Dockerfile index c2b65d0..c0edaa7 100644 --- a/package/docker/Dockerfile +++ b/package/docker/Dockerfile @@ -1,13 +1,13 @@ -FROM python:3.8-alpine +FROM python:3.9-alpine ADD spaceapi_server/ /spaceapi_server/ ADD setup.py /setup.py RUN python setup.py install && mkdir -p /config/plugins && chmod 0755 -R /config -ADD package/docker/config.json /config/config.json -ADD package/docker/template.json /config/template.json +ADD package/docker/config.yaml /config/config.yaml +ADD package/docker/template.yaml /config/template.yaml VOLUME /config EXPOSE 8000/tcp 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" ] diff --git a/package/docker/config.json b/package/docker/config.json deleted file mode 100644 index 38107cf..0000000 --- a/package/docker/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "address": "0.0.0.0", - "port": 8000, - "server": "wsgiref", - "template": "/config/template.json", - "plugins_dir": "/config/plugins", - "plugins": {} -} diff --git a/package/docker/config.yaml b/package/docker/config.yaml new file mode 100644 index 0000000..25033a3 --- /dev/null +++ b/package/docker/config.yaml @@ -0,0 +1,6 @@ +--- +address: "::" +port: 8080 +template: /etc/spaceapi-server/template.json +plugins_dir: /etc/spaceapi-server/plugins +plugins: {} diff --git a/package/docker/template.json b/package/docker/template.json deleted file mode 100644 index 4647ff5..0000000 --- a/package/docker/template.json +++ /dev/null @@ -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" - ] -} diff --git a/package/docker/template.yaml b/package/docker/template.yaml new file mode 100644 index 0000000..d331863 --- /dev/null +++ b/package/docker/template.yaml @@ -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 diff --git a/setup.py b/setup.py index 3fae7ce..732c6ae 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( python_requires='>=3.6', install_requires=[ 'bottle', - 'jinja2==2.10' + 'PyYAML', ], entry_points={ 'console_scripts': [ @@ -31,7 +31,7 @@ setup( 'Environment :: Web Environment', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application' ] ) diff --git a/spaceapi_server/__init__.py b/spaceapi_server/__init__.py index 81cfc8c..b1f5cd4 100644 --- a/spaceapi_server/__init__.py +++ b/spaceapi_server/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.3.1' +__version__ = '0.4' diff --git a/spaceapi_server/config/__init__.py b/spaceapi_server/config/__init__.py index 01ac10f..7ecc0e4 100644 --- a/spaceapi_server/config/__init__.py +++ b/spaceapi_server/config/__init__.py @@ -1,5 +1,5 @@ -import json +import yaml # The parsed config object @@ -8,13 +8,13 @@ __CONFIG = {} def load(filename: str) -> None: """ - Load a JSON-formatted configuration file. + Load a YAML-formatted configuration file. :param filename: The config file to load. """ global __CONFIG - # Open and parse the JSON config file + # Open and parse the YAML config file with open(filename, 'r') as conf: - __CONFIG = json.load(conf) + __CONFIG = yaml.safe_load(conf) def get() -> dict: diff --git a/spaceapi_server/config/test/__init__.py b/spaceapi_server/config/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/spaceapi_server/config/test/test_config.py b/spaceapi_server/config/test/test_config.py deleted file mode 100644 index ec0df6b..0000000 --- a/spaceapi_server/config/test/test_config.py +++ /dev/null @@ -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']) diff --git a/spaceapi_server/plugins/__init__.py b/spaceapi_server/plugins/__init__.py index d6b52fc..f09188f 100644 --- a/spaceapi_server/plugins/__init__.py +++ b/spaceapi_server/plugins/__init__.py @@ -1,4 +1,6 @@ +import yaml + from spaceapi_server import config, template @@ -7,22 +9,8 @@ def template_function(fn): Register the decorated function as a callable template function. :param fn: The function to register. """ - # Make sure the Jinja2 environment is initialized - env = template._env_init() - # 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 + yaml.SafeLoader.add_constructor(f'!{fn.__name__}', template.plugin_constructor) + template.PluginInvocation.register_plugin(fn.__name__, fn) return fn diff --git a/spaceapi_server/server/__init__.py b/spaceapi_server/server/__init__.py index af00c60..cd90d93 100644 --- a/spaceapi_server/server/__init__.py +++ b/spaceapi_server/server/__init__.py @@ -1,6 +1,7 @@ import os import sys import json +import yaml import signal import importlib @@ -51,10 +52,10 @@ def load(*args, **kwargs): m_spec.loader.exec_module(module) # Get the template path - template_path = conf.get('template', 'template.json') + template_path = conf.get('template', 'template.yaml') # Load and parse the JSON template with open(template_path, 'r') as f: - __TEMPLATE = json.load(f) + __TEMPLATE = yaml.safe_load(f) def init(): diff --git a/spaceapi_server/template/__init__.py b/spaceapi_server/template/__init__.py index 57f985a..66ce025 100644 --- a/spaceapi_server/template/__init__.py +++ b/spaceapi_server/template/__init__.py @@ -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 -__ENV = None - - -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 plugin_constructor(loader, node): + data = loader.construct_mapping(node) + return PluginInvocation(node.tag[1:], data) def render_traverse(obj): """ - Walk through a complex, JSON-serializable data structure, and pass - string objects through the Jinja2 templating engine. + Walk through a complex, JSON-serializable data structure, and + invoke plugins if for custom tags. :param obj: The object to traverse. """ if isinstance(obj, list): @@ -47,9 +34,9 @@ def render_traverse(obj): elif isinstance(obj, dict): # dict -> recurse into the value of each (key, value) return {k: render_traverse(v) for k, v in obj.items()} - elif isinstance(obj, str): - # str -> template - return render(obj) + elif isinstance(obj, PluginInvocation): + # PluginTag -> invoke the plugin with the stored arguments + return obj() else: # anything else -> return as-is return obj diff --git a/spaceapi_server/template/test/__init__.py b/spaceapi_server/template/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/spaceapi_server/template/test/test_template.py b/spaceapi_server/template/test/test_template.py deleted file mode 100644 index 3c51076..0000000 --- a/spaceapi_server/template/test/test_template.py +++ /dev/null @@ -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])
-```json -{ - "api": "0.13", - "space": "My Hackerspace", - "state": "{{ space_state() }}", - "sensors": { - "network_connections": "{{ network_connections() }}" - } -} +```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" ] ```