Replace Jinja2 with PyYAML
This commit is contained in:
parent
a3eb452786
commit
093a40fefa
31 changed files with 197 additions and 376 deletions
|
@ -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
|
||||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -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
|
||||||
|
|
||||||
|
|
173
README.md
173
README.md
|
@ -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.
|
||||||
# Plugin-specific configuration should go in here, separated by plugin
|
template: template.yaml
|
||||||
"my_plugin": {
|
# Path to the directory containing your plugins.
|
||||||
"my_option": "Hello, World!",
|
plugins_dir: plugins
|
||||||
"my_other_option": [ 42, 1337 ]
|
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
|
### 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/
|
||||||
|
|
|
@ -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 \
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"address": "::",
|
|
||||||
"port": 8080,
|
|
||||||
"template": "/etc/spaceapi-server/template.json",
|
|
||||||
"plugins_dir": "/etc/spaceapi-server/plugins",
|
|
||||||
"plugins": {}
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
address: "::"
|
||||||
|
port: 8080
|
||||||
|
template: /etc/spaceapi-server/template.json
|
||||||
|
plugins_dir: /etc/spaceapi-server/plugins
|
||||||
|
plugins: {}
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"address": "::",
|
|
||||||
"port": 8080,
|
|
||||||
"template": "/etc/spaceapi-server/template.json",
|
|
||||||
"plugins_dir": "/etc/spaceapi-server/plugins",
|
|
||||||
"plugins": {}
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
address: "::"
|
||||||
|
port: 8080
|
||||||
|
template: /etc/spaceapi-server/template.json
|
||||||
|
plugins_dir: /etc/spaceapi-server/plugins
|
||||||
|
plugins: {}
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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" ]
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"address": "0.0.0.0",
|
|
||||||
"port": 8000,
|
|
||||||
"server": "wsgiref",
|
|
||||||
"template": "/config/template.json",
|
|
||||||
"plugins_dir": "/config/plugins",
|
|
||||||
"plugins": {}
|
|
||||||
}
|
|
6
package/docker/config.yaml
Normal file
6
package/docker/config.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
address: "::"
|
||||||
|
port: 8080
|
||||||
|
template: /etc/spaceapi-server/template.json
|
||||||
|
plugins_dir: /etc/spaceapi-server/plugins
|
||||||
|
plugins: {}
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
14
package/docker/template.yaml
Normal file
14
package/docker/template.yaml
Normal 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
|
4
setup.py
4
setup.py
|
@ -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'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
|
|
||||||
__version__ = '0.3.1'
|
__version__ = '0.4'
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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'])
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])
|
|
Loading…
Reference in a new issue