Compare commits
36 commits
Author | SHA1 | Date | |
---|---|---|---|
9cd927cc8e | |||
2c29c725e5 | |||
4435ec6dcf | |||
c7e3a79244 | |||
16f223d423 | |||
8f21eb95e6 | |||
f0b6bc7d66 | |||
|
03b4beef92 | ||
|
f851170231 | ||
|
a6eb99f084 | ||
|
00cd0ef70b | ||
|
ec8cd80063 | ||
|
882ba34e47 | ||
|
1521aa96ce | ||
|
429a48bff4 | ||
|
0018e11392 | ||
|
5ad9d309b8 | ||
|
826a108308 | ||
|
7557183a3a | ||
|
aa05cfa296 | ||
|
093a40fefa | ||
|
a3eb452786 | ||
|
9959d5ca49 | ||
|
5906c201ca | ||
|
c925281039 | ||
|
6b957ac864 | ||
|
b5f5c8f32b | ||
|
e8c35f697d | ||
|
79fae47918 | ||
|
d210e05d7e | ||
|
e33d0d03d6 | ||
|
45384b2a62 | ||
|
138234c4a3 | ||
|
f6ad53555c | ||
|
6a84976d84 | ||
|
5c6065996e |
40 changed files with 627 additions and 621 deletions
43
.forgejo/workflows/package.yml
Normal file
43
.forgejo/workflows/package.yml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build_wheel:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
- name: Build Python wheel
|
||||||
|
run: |
|
||||||
|
apt update; apt install -y python3-pip
|
||||||
|
pip3 install --break-system-packages -e .[test]
|
||||||
|
python3 setup.py egg_info bdist_wheel
|
||||||
|
- uses: https://git.kabelsalat.ch/s3lph/forgejo-action-wheel-package-upload@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.API_USERNAME }}
|
||||||
|
password: ${{ secrets.API_PASSWORD }}
|
||||||
|
|
||||||
|
build_debian:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
- name: Prepare package
|
||||||
|
run: |
|
||||||
|
find package/debian -name .gitkeep -delete
|
||||||
|
# Copy example plugin
|
||||||
|
install -m0644 examples/plugins/filestate.py package/debian/spaceapi-server/etc/spaceapi-server/plugins/filestate.py
|
||||||
|
- uses: https://git.kabelsalat.ch/s3lph/forgejo-action-python-debian-package@v4
|
||||||
|
with:
|
||||||
|
python_module: spaceapi_server
|
||||||
|
package_name: spaceapi-server
|
||||||
|
package_root: package/debian/spaceapi-server
|
||||||
|
package_output_path: package/debian
|
||||||
|
- uses: https://git.kabelsalat.ch/s3lph/forgejo-action-debian-package-upload@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.API_USERNAME }}
|
||||||
|
password: ${{ secrets.API_PASSWORD }}
|
||||||
|
deb: "package/debian/*.deb"
|
27
.forgejo/workflows/test.yml
Normal file
27
.forgejo/workflows/test.yml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
- name: test
|
||||||
|
run: |
|
||||||
|
apt update; apt install --yes python3-pip
|
||||||
|
pip3 install --break-system-packages -e .[test]
|
||||||
|
python3 -m coverage run --rcfile=setup.cfg -m unittest discover spaceapi_server
|
||||||
|
python3 -m coverage combine
|
||||||
|
python3 -m coverage report --rcfile=setup.cfg
|
||||||
|
|
||||||
|
codestyle:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
- name: codestyle
|
||||||
|
run: |
|
||||||
|
apt update; apt install --yes python3-pip
|
||||||
|
pip3 install --break-system-packages -e .[test]
|
||||||
|
pycodestyle spaceapi_server
|
151
.gitlab-ci.yml
151
.gitlab-ci.yml
|
@ -1,151 +0,0 @@
|
||||||
---
|
|
||||||
image: s3lph/spaceapi-server-ci:20191126-02
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- test
|
|
||||||
- build
|
|
||||||
- release
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- export SPACEAPI_SERVER_VERSION=$(python -c 'import spaceapi_server; print(spaceapi_server.__version__)')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- pip3 install -e .
|
|
||||||
- python3 -m coverage run --rcfile=setup.cfg -m unittest discover spaceapi_server
|
|
||||||
- python3 -m coverage combine
|
|
||||||
- python3 -m coverage report --rcfile=setup.cfg
|
|
||||||
|
|
||||||
codestyle:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- pip3 install -e .
|
|
||||||
- pycodestyle spaceapi_server
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build_wheel:
|
|
||||||
stage: build
|
|
||||||
script:
|
|
||||||
- python3 setup.py egg_info bdist_wheel
|
|
||||||
- cd dist
|
|
||||||
- sha256sum *.whl > SHA256SUMS
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- "dist/*.whl"
|
|
||||||
- dist/SHA256SUMS
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
|
|
||||||
build_debian:
|
|
||||||
stage: build
|
|
||||||
script:
|
|
||||||
- find package/debian -name .gitkeep -delete
|
|
||||||
# Copy example plugin
|
|
||||||
- install -m0644 examples/plugins/example.py package/debian/spaceapi-server/etc/spaceapi-server/plugins/example.py
|
|
||||||
# Create control
|
|
||||||
- |
|
|
||||||
cat > package/debian/spaceapi-server/DEBIAN/control <<EOF
|
|
||||||
Package: spaceapi-server
|
|
||||||
Version: ${SPACEAPI_SERVER_VERSION}
|
|
||||||
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Architecture: all
|
|
||||||
Depends: python3 (>= 3.6), python3-jinja2, 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.
|
|
||||||
EOF
|
|
||||||
- chmod 0644 package/debian/spaceapi-server/DEBIAN/control
|
|
||||||
# Create changelog
|
|
||||||
- |
|
|
||||||
for version in "$(cat CHANGELOG.md | grep '<!-- BEGIN CHANGES' | cut -d ' ' -f 4)"; do
|
|
||||||
echo "spaceapi-server (${version}-1); urgency=medium\n" >> package/debian/spaceapi-server/usr/share/doc/spaceapi-server/changelog
|
|
||||||
cat CHANGELOG.md | grep -A 1000 "<"'!'"-- BEGIN CHANGES ${version} -->" | grep -B 1000 "<"'!'"-- END CHANGES ${version} -->" | tail -n +2 | head -n -1 | sed -re 's/^-/ */g' >> package/debian/spaceapi-server/usr/share/doc/spaceapi-server/changelog
|
|
||||||
echo "\n -- s3lph <account-gitlab-ideynizv@kernelpanic.lol> $(date -R)\n" >> package/debian/spaceapi-server/usr/share/doc/spaceapi-server/changelog
|
|
||||||
done
|
|
||||||
- gzip -9n package/debian/spaceapi-server/usr/share/doc/spaceapi-server/changelog
|
|
||||||
# Copy license
|
|
||||||
- install -m0644 LICENSE package/debian/spaceapi-server/usr/share/doc/spaceapi-server/copyright
|
|
||||||
# Install spaceapi-server into package root
|
|
||||||
- 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/
|
|
||||||
# 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 {} \;
|
|
||||||
# Remove spaceapi-server script
|
|
||||||
- rm usr/bin/spaceapi-server
|
|
||||||
# Fix file permissions
|
|
||||||
- find . -type f -exec chmod 0644 {} \;
|
|
||||||
- find . -type d -exec chmod 755 {} \;
|
|
||||||
- chmod 0755 DEBIAN/postinst DEBIAN/prerm DEBIAN/postrm
|
|
||||||
# Build the package
|
|
||||||
- cd ..
|
|
||||||
- dpkg-deb --build spaceapi-server
|
|
||||||
- mv spaceapi-server.deb "spaceapi-server_${SPACEAPI_SERVER_VERSION}-1_all.deb"
|
|
||||||
# Run lintian
|
|
||||||
- sudo -u nobody lintian --fail-on-warnings "spaceapi-server_${SPACEAPI_SERVER_VERSION}-1_all.deb"
|
|
||||||
# Generate checksum
|
|
||||||
- sha256sum *.deb > SHA256SUMS
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- "package/debian/*.deb"
|
|
||||||
- package/debian/SHA256SUMS
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
|
|
||||||
build_archlinux:
|
|
||||||
stage: build
|
|
||||||
image: archlinux/base:latest # Use an archlinux image instead of the customized debian image.
|
|
||||||
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
|
|
||||||
- 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
|
|
||||||
# Create changelog
|
|
||||||
- |
|
|
||||||
for version in "$(cat CHANGELOG.md | grep '<!-- BEGIN CHANGES' | cut -d ' ' -f 4)"; do
|
|
||||||
echo "$(date +%Y-%m-%d) s3lph <account-gitlab-ideynizv@kernelpanic.lol>" >> package/archlinux/spaceapi-server.changelog
|
|
||||||
cat CHANGELOG.md | grep -A 1000 "<"'!'"-- BEGIN CHANGES ${version} -->" | grep -B 1000 "<"'!'"-- END CHANGES ${version} -->" | tail -n +2 | head -n -1 | sed -re 's/^-/\t*/g' >> package/archlinux/spaceapi-server.changelog
|
|
||||||
echo >> package/archlinux/spaceapi-server.changelog
|
|
||||||
done
|
|
||||||
# Copy license
|
|
||||||
- install -m0644 LICENSE package/archlinux/spaceapi-server/usr/share/licenses/spaceapi-server/LICENSE
|
|
||||||
# Install spaceapi-server into pkgdir
|
|
||||||
- python setup.py egg_info -d -b +master install --root=package/archlinux/spaceapi-server/ --prefix=/usr --optimize=1
|
|
||||||
- cd package/archlinux
|
|
||||||
# Remove spaceapi-server script
|
|
||||||
- rm -rf spaceapi-server/usr/bin
|
|
||||||
# Build the package
|
|
||||||
- sed -re "s/__VERSION__/${SPACEAPI_SERVER_VERSION}/g" -i PKGBUILD
|
|
||||||
- sudo -u nobody makepkg -c
|
|
||||||
# Run namcap
|
|
||||||
- sudo -u nobody namcap *.pkg.tar.xz
|
|
||||||
# Generate checksum
|
|
||||||
- sha256sum *.pkg.tar.xz > SHA256SUMS
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- "package/archlinux/*.pkg.tar.xz"
|
|
||||||
- package/archlinux/SHA256SUMS
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
script:
|
|
||||||
- python3 package/release.py
|
|
||||||
only:
|
|
||||||
- tags
|
|
107
CHANGELOG.md
107
CHANGELOG.md
|
@ -1,5 +1,112 @@
|
||||||
# SpaceAPI Server Changelog
|
# SpaceAPI Server Changelog
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.5.5 -->
|
||||||
|
## Version 0.5.5
|
||||||
|
|
||||||
|
Bugfix & Maintenance release
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.5.5 -->
|
||||||
|
- Add SpaceAPI Schema v14 in example
|
||||||
|
- Fix template filename in example config.yaml
|
||||||
|
<!-- END CHANGES 0.5.5 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.5.5 -->
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.5.4 -->
|
||||||
|
## Version 0.5.4
|
||||||
|
|
||||||
|
Maintenance build
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.5.4 -->
|
||||||
|
- Migration from Woodpecker to Forgejo Actions
|
||||||
|
<!-- END CHANGES 0.5.4 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.5.4 -->
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.5.2 -->
|
||||||
|
## Version 0.5.2
|
||||||
|
|
||||||
|
Maintenance build
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.5.2 -->
|
||||||
|
- Migration from Gitlab to Gitea+Woodpecker
|
||||||
|
<!-- END CHANGES 0.5.2 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.5.2 -->
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.5.1 -->
|
||||||
|
## Version 0.5.1
|
||||||
|
|
||||||
|
Maintenance build
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.5.1 -->
|
||||||
|
- Fix: CI & Release process
|
||||||
|
<!-- END CHANGES 0.5.1 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.5.1 -->
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.5 -->
|
||||||
|
## Version 0.5
|
||||||
|
|
||||||
|
Update Readme, fix systemd unit
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.5 -->
|
||||||
|
- Fix: Systemd unit now depends on multi-user.target
|
||||||
|
- Fix smaller issues in Readme and update for SpaceAPI v14
|
||||||
|
<!-- END CHANGES 0.5 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.5 -->
|
||||||
|
|
||||||
|
<!-- 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 -->
|
||||||
|
## Version 0.3
|
||||||
|
|
||||||
|
Container image release
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.3.1 -->
|
||||||
|
- Release as container image
|
||||||
|
<!-- END CHANGES 0.3.1 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.3.1 -->
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.3 -->
|
||||||
|
## Version 0.3
|
||||||
|
|
||||||
|
Feature Removal Release
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.3 -->
|
||||||
|
- Remove template_test decorator
|
||||||
|
<!-- END CHANGES 0.3 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.3 -->
|
||||||
|
|
||||||
<!-- BEGIN RELEASE v0.2 -->
|
<!-- BEGIN RELEASE v0.2 -->
|
||||||
## Version 0.2
|
## Version 0.2
|
||||||
|
|
||||||
|
|
303
README.md
303
README.md
|
@ -1,64 +1,173 @@
|
||||||
# SpaceAPI Server
|
# SpaceAPI Server
|
||||||
|
|
||||||
[![pipeline status](https://gitlab.com/s3lph/spaceapi-server/badges/master/pipeline.svg)][master]
|
A lightweight server for [SpaceAPI][spaceapi] endpoints. Includes
|
||||||
[![coverage report](https://gitlab.com/s3lph/spaceapi-server/badges/master/coverage.svg)][master]
|
support for pluggable templating, so dynamic content, like sensor
|
||||||
|
values, can be added.
|
||||||
A lightweight server for [SpaceAPI][spaceapi] endpoints. Includes support for pluggable templating, so dynamic content,
|
|
||||||
like sensor values, can be added.
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- Python 3 (>=3.6)
|
- Python 3 (>=3.6)
|
||||||
- [Bottle][pypi-bottle]
|
- [Bottle][pypi-bottle]
|
||||||
- [Jinja2][pypi-jinja2]
|
- [PyYAML][pypi-yaml]
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT License][mit]
|
[MIT License][mit]
|
||||||
|
|
||||||
## Usage
|
## Introduction
|
||||||
|
|
||||||
```bash
|
This project is an attempt to implement a lightweight, yet versatile
|
||||||
python -m spaceapi_server config.json
|
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 YAML document (anything
|
||||||
|
except object keys) with custom plugin invocations. These plugins
|
||||||
|
look up and return your dynamic content.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Input</td>
|
||||||
|
<td>Output</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
api: "0.13"
|
||||||
|
api_compatibility: ["14"]
|
||||||
|
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" ]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"address": "::1", # The address to listen on.
|
"api": "0.13",
|
||||||
"port": 8000, # The TCP port to listen on.
|
"api_compatibility": ["14"],
|
||||||
"server": "wsgiref", # The Bottle backend server to use.
|
"space": "My Hackerspace",
|
||||||
"template": "template.json", # Path to the SpaceAPI response template file.
|
"state": {
|
||||||
"plugins_dir": "plugins", # Path to the directory containing your plugins.
|
"open": true,
|
||||||
|
"lastchange": 1575160777,
|
||||||
"plugins": {
|
"message": "Visitors Welcome!"
|
||||||
# Plugin-specific configuration should go in here, e.g. `.plugins.sqlite.database`
|
},
|
||||||
}
|
"sensors": {
|
||||||
|
"network_connections": [
|
||||||
|
{
|
||||||
|
"value": 4,
|
||||||
|
"type": "wifi",
|
||||||
|
"name": "2.4 GHz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 7,
|
||||||
|
"type": "wifi",
|
||||||
|
"name": "5 GHz"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Serve a Static SpaceAPI Endpoint
|
## Usage
|
||||||
|
|
||||||
Have a look at [SpaceAPI: Getting Started][spaceapi-getting-started]. If you only want to serve static content, your
|
### 0. Download
|
||||||
`template.json` may consist of regular JSON data, which is served almost-as-is (once parsed and re-serialized).
|
|
||||||
|
|
||||||
## Add Dynamic Content
|
Head over to the [Packages][packages] tab, download and install the
|
||||||
|
package that suits your needs or set up the Debian repository.
|
||||||
|
Alternatively, clone the repo and get started.
|
||||||
|
|
||||||
This example guides you through adding a dynamic `.state` property to your SpaceAPI endpoint. We'll use the following
|
The remainder of this document assumes that you installed the
|
||||||
(rather simple, and probably not too useful) data source: Check a certain file, and mark the space as open depending on
|
server as a Debian package.
|
||||||
its existence.
|
|
||||||
|
|
||||||
1. Create a plugin to fetch the data. Let's name it `mybackend.py` and put in in our plugins directory:
|
### 1. Overview
|
||||||
|
|
||||||
|
The configuration of this server consists of three parts:
|
||||||
|
|
||||||
|
- The **main configuration** file, usually located at
|
||||||
|
`/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.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
|
||||||
|
rendering dynamic content.
|
||||||
|
|
||||||
|
### 2. Configure the Server
|
||||||
|
|
||||||
|
Open the file `/etc/spaceapi-server/config.yaml`.
|
||||||
|
|
||||||
|
The following options are currently available:
|
||||||
|
|
||||||
|
```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.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" (apart from the conversion from
|
||||||
|
YAML to JSON).
|
||||||
|
|
||||||
|
To learn about how a SpaceAPI response should look like, have a look
|
||||||
|
at the [SpaceAPI Website][spaceapi].
|
||||||
|
|
||||||
|
### 4. Add Dynamic Content
|
||||||
|
|
||||||
|
This example guides you through adding a dynamic `state` property to
|
||||||
|
your SpaceAPI endpoint. We'll use the following (rather simple, and
|
||||||
|
probably not too useful) data source: Check a certain file, and mark
|
||||||
|
the space as open depending on its existence.
|
||||||
|
|
||||||
|
1. Create a plugin to fetch the data. Let's name it `filestate.py`
|
||||||
|
and put in in our plugins directory:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
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('mybackend')
|
conf = config.get_plugin_config('filestate')
|
||||||
# Get the filename
|
# Get the filename
|
||||||
filename = conf.get('filename', '/var/space_state')
|
filename = conf.get('filename', '/var/space_state')
|
||||||
try:
|
try:
|
||||||
|
@ -76,44 +185,128 @@ its existence.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `@template_function` decorator registers the function as a callable in Jinja's globals. There's also
|
The `@template_function` decorator registers a constructor in
|
||||||
`@template_filter`, which registers a Jinja2 filter, and `@template_test`, which registers a test. For more
|
PyYAML's parser with the function's name as tag
|
||||||
information on the Jinja2 templating engine, see [Jinja2][jinja].
|
(e.g. `!space_state).
|
||||||
|
|
||||||
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:
|
||||||
"mybackend": {
|
filestate:
|
||||||
"filename", "/var/space_state"
|
filename: /var/space_state
|
||||||
}
|
|
||||||
}
|
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Start the server and query it.
|
### 5. Start the Server
|
||||||
|
|
||||||
[master]: https://gitlab.com/s3lph/spaceapi-server/commits/master
|
Start the server with e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl start spaceapi-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
To reload the configuration, template and plugins, send a SIGHUP,
|
||||||
|
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.yaml
|
||||||
|
```
|
||||||
|
### 6. Test the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
You should be greeted with the SpaceAPI endpoint response.
|
||||||
|
|
||||||
|
## Plugin API Reference
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The following functions provide access to values defined in the
|
||||||
|
configuration file.
|
||||||
|
|
||||||
|
#### `spaceapi_server.config.get_plugin_config(name: str)`
|
||||||
|
|
||||||
|
This function returns a plugin's configuration.
|
||||||
|
|
||||||
|
The function takes one argument, the name of the plugin. This name is
|
||||||
|
used to look up the plugin configuration.
|
||||||
|
|
||||||
|
The function returns the content present at the key `.plugins.<name>`
|
||||||
|
of the global configuration file, or an empty object if absent.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from spaceapi_server import plugins
|
||||||
|
|
||||||
|
print(plugins.get_plugin_config('my_plugin'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Templating
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
If performance is an issue, consider applying caching, either in your
|
||||||
|
plugins, or by using a caching HTTP reverse proxy.
|
||||||
|
|
||||||
|
#### `spaceapi_server.plugins.template_function`
|
||||||
|
|
||||||
|
This decorator registers the function's name as a YAML tag in the parser.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from spaceapi_server import plugins
|
||||||
|
|
||||||
|
@plugins.template_function
|
||||||
|
def lookup_sensor(query, default=None):
|
||||||
|
# Do something with the query
|
||||||
|
result = ...
|
||||||
|
# If the lookup failed, return a default value
|
||||||
|
if not result:
|
||||||
|
return default or []
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
state: !lookup_sensor
|
||||||
|
query: SELECT timestamp, value FROM people_now_present LIMIT 1
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[main]: https://gitlab.com/s3lph/spaceapi-server/commits/main
|
||||||
|
[packages]: https://git.kabelsalat.ch/s3lph/spaceapi-server/packages
|
||||||
[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/main/LICENSE
|
||||||
[spaceapi-getting-started]: https://spaceapi.io/getting-started/
|
[jinja]: https://jinja.palletsprojects.com/
|
||||||
[jinja]: https://jinja.palletsprojects.com/
|
[registry]: https://gitlab.com/s3lph/spaceapi-server/container_registry
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"address": "::1",
|
|
||||||
"port": 8000,
|
|
||||||
"template": "examples/template.json",
|
|
||||||
"plugins_dir": "examples/plugins",
|
|
||||||
|
|
||||||
"plugins": {
|
|
||||||
"example": {
|
|
||||||
"test_value": "the Spanish Inquisition"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
16
examples/config.yaml
Normal file
16
examples/config.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
# 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:
|
||||||
|
# Config for the "filestate" plugin
|
||||||
|
filestate:
|
||||||
|
# Use this statefile instead of the default
|
||||||
|
filename: /var/www/html/space_state
|
|
@ -1,40 +0,0 @@
|
||||||
|
|
||||||
from spaceapi_server import config, plugins
|
|
||||||
|
|
||||||
|
|
||||||
@plugins.template_function
|
|
||||||
def example_function(name: str):
|
|
||||||
"""
|
|
||||||
This function is registered as a Jinja2 function. It can be used like this:
|
|
||||||
{{ example_function('the Spanish Inquisition') }}
|
|
||||||
"""
|
|
||||||
return f'Nobody expects {name}'
|
|
||||||
|
|
||||||
|
|
||||||
@plugins.template_filter
|
|
||||||
def example_filter(name: str):
|
|
||||||
"""
|
|
||||||
This function is registered as a Jinja2 filter. It can be used like this:
|
|
||||||
{{ 'the Spanish Inquisition' | example_filter }}
|
|
||||||
"""
|
|
||||||
return f'Nobody expects {name}'
|
|
||||||
|
|
||||||
|
|
||||||
@plugins.template_test
|
|
||||||
def example_test(name: str):
|
|
||||||
"""
|
|
||||||
This function is registered as a Jinja2 test. It can be used like this:
|
|
||||||
{% if 'the Spanish Inquisition' is example_test %}
|
|
||||||
"""
|
|
||||||
# Config lookup example. A plugin's config should be below
|
|
||||||
# `.plugins[plugin_name]` (JSONPath)
|
|
||||||
|
|
||||||
# Get the .plugins.example dict
|
|
||||||
conf = config.get_plugin_config('example')
|
|
||||||
# Get the .test_value property from the plugin config, falling
|
|
||||||
# back to a default value
|
|
||||||
test_value = conf.get('test_value', 'the Spanish Inquisition')
|
|
||||||
|
|
||||||
# Tests must always return a boolean or boolean-castable
|
|
||||||
# expression
|
|
||||||
return name == test_value
|
|
23
examples/plugins/filestate.py
Normal file
23
examples/plugins/filestate.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import os
|
||||||
|
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')
|
||||||
|
# Get the filename, default to /var/space_state
|
||||||
|
filename = conf.get('filename', '/var/space_state')
|
||||||
|
try:
|
||||||
|
# Get the file's properties
|
||||||
|
stat = os.stat(filename)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# File doesn't exist, aka. space is closed
|
||||||
|
return {
|
||||||
|
'open': False
|
||||||
|
}
|
||||||
|
# File exists, aka. space is open. Also report the mtime as "last changed" timestamp
|
||||||
|
return {
|
||||||
|
'open': True,
|
||||||
|
'lastchange': int(stat.st_mtime)
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"api": "0.13 {#- Go look at https://spaceapi.io/docs -#}",
|
|
||||||
"space": "Our New Hackerspace",
|
|
||||||
"logo": "https://example.org/logo.png",
|
|
||||||
"url": "https://example.org/",
|
|
||||||
"location": {
|
|
||||||
"lat": 0.0,
|
|
||||||
"lon": 0.0
|
|
||||||
},
|
|
||||||
"contact": {
|
|
||||||
"email": "example@example.org"
|
|
||||||
},
|
|
||||||
"issue_report_channels": [
|
|
||||||
"email"
|
|
||||||
],
|
|
||||||
"state": "{#- You can write your own plugins for retrieving dynamic information -#} {{ our_space_state() }}",
|
|
||||||
"sensors": {
|
|
||||||
"people_now_present": "{{ our_sensor_backend('people_count') }}"
|
|
||||||
}
|
|
||||||
}
|
|
12
examples/template.yaml
Normal file
12
examples/template.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
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" ]
|
|
@ -1,6 +0,0 @@
|
||||||
FROM python:3.8-buster as python
|
|
||||||
|
|
||||||
RUN apt update \
|
|
||||||
&& apt install -y --no-install-recommends lintian rsync sudo \
|
|
||||||
&& pip3 install pycodestyle coverage \
|
|
||||||
&& rm -rf /var/cache/apt
|
|
|
@ -1,27 +0,0 @@
|
||||||
# Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
|
||||||
pkgname=spaceapi-server
|
|
||||||
pkgver=__VERSION__
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="Lightweight SpaceAPI endpoint server"
|
|
||||||
arch=('any')
|
|
||||||
url="https://gitlab.com/s3lph/spaceapi-server"
|
|
||||||
license=('MIT')
|
|
||||||
groups=()
|
|
||||||
depends=('python'
|
|
||||||
'python-jinja'
|
|
||||||
'python-bottle')
|
|
||||||
makedepends=('python-setuptools')
|
|
||||||
checkdepends=()
|
|
||||||
optdepends=()
|
|
||||||
provides=()
|
|
||||||
conflicts=()
|
|
||||||
replaces=()
|
|
||||||
backup=('etc/spaceapi-server/config.json'
|
|
||||||
'etc/spaceapi-server/template.json'
|
|
||||||
'etc/spaceapi-server/plugins')
|
|
||||||
install=$pkgname.install
|
|
||||||
changelog=$pkgname.changelog
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cp -r ../spaceapi-server/* ../pkg/spaceapi-server/
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
|
|
||||||
post_install() {
|
|
||||||
|
|
||||||
if ! getent group spaceapi-server >/dev/null; then
|
|
||||||
groupadd --system spaceapi-server
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! getent passwd spaceapi-server >/dev/null; then
|
|
||||||
useradd --system --create-home --gid spaceapi-server --home-dir /var/lib/spaceapi-server \
|
|
||||||
--shell /usr/sbin/nologin spaceapi-server
|
|
||||||
fi
|
|
||||||
|
|
||||||
chown root:spaceapi-server /etc/spaceapi-server
|
|
||||||
chmod 0750 /etc/spaceapi-server
|
|
||||||
|
|
||||||
systemctl daemon-reload || true
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pre_remove() {
|
|
||||||
|
|
||||||
systemctl stop spaceapi-server.service
|
|
||||||
userdel spaceapi-server
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
post_remove() {
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Lightweight SpaceAPI Endpoint Server
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart=/usr/bin/python -m spaceapi_server /etc/spaceapi-server/config.json
|
|
||||||
ExecReload=/usr/bin/kill -HUP $MAINPID
|
|
||||||
User=spaceapi-server
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=network-online.target
|
|
|
@ -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/filestate.py
|
||||||
|
|
10
package/debian/spaceapi-server/DEBIAN/control
Normal file
10
package/debian/spaceapi-server/DEBIAN/control
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Package: spaceapi-server
|
||||||
|
Version: __VERSION__
|
||||||
|
Maintainer: s3lph <s3lph@kabelsalat.ch>
|
||||||
|
Section: web
|
||||||
|
Priority: optional
|
||||||
|
Architecture: all
|
||||||
|
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.
|
|
@ -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.yaml
|
||||||
|
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,16 @@
|
||||||
|
---
|
||||||
|
api: "0.13"
|
||||||
|
api_compatibility:
|
||||||
|
- "14"
|
||||||
|
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,9 +2,9 @@
|
||||||
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
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=network-online.target
|
WantedBy=multi-user.target
|
13
package/docker/Dockerfile
Normal file
13
package/docker/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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.yaml /config/config.yaml
|
||||||
|
ADD package/docker/template.yaml /config/template.yaml
|
||||||
|
|
||||||
|
VOLUME /config
|
||||||
|
EXPOSE 8000/tcp
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/usr/local/bin/python3.9", "-m", "spaceapi_server", "/config/config.yaml" ]
|
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: {}
|
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
|
|
@ -10,6 +10,9 @@ import http.client
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
USER_AGENT = 'curl/7.70.0'
|
||||||
|
|
||||||
|
|
||||||
def parse_changelog(tag: str) -> Optional[str]:
|
def parse_changelog(tag: str) -> Optional[str]:
|
||||||
release_changelog: str = ''
|
release_changelog: str = ''
|
||||||
with open('CHANGELOG.md', 'r') as f:
|
with open('CHANGELOG.md', 'r') as f:
|
||||||
|
@ -32,7 +35,8 @@ def parse_changelog(tag: str) -> Optional[str]:
|
||||||
def fetch_job_ids(project_id: int, pipeline_id: int, api_token: str) -> Dict[str, str]:
|
def fetch_job_ids(project_id: int, pipeline_id: int, api_token: str) -> Dict[str, str]:
|
||||||
url: str = f'https://gitlab.com/api/v4/projects/{project_id}/pipelines/{pipeline_id}/jobs'
|
url: str = f'https://gitlab.com/api/v4/projects/{project_id}/pipelines/{pipeline_id}/jobs'
|
||||||
headers: Dict[str, str] = {
|
headers: Dict[str, str] = {
|
||||||
'Private-Token': api_token
|
'Private-Token': api_token,
|
||||||
|
'User-Agent': USER_AGENT
|
||||||
}
|
}
|
||||||
req = urllib.request.Request(url, headers=headers)
|
req = urllib.request.Request(url, headers=headers)
|
||||||
try:
|
try:
|
||||||
|
@ -52,7 +56,10 @@ def fetch_job_ids(project_id: int, pipeline_id: int, api_token: str) -> Dict[str
|
||||||
|
|
||||||
|
|
||||||
def fetch_single_shafile(url: str) -> str:
|
def fetch_single_shafile(url: str) -> str:
|
||||||
req = urllib.request.Request(url)
|
headers: Dict[str, str] = {
|
||||||
|
'User-Agent': USER_AGENT
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
try:
|
try:
|
||||||
resp: http.client.HTTPResponse = urllib.request.urlopen(req)
|
resp: http.client.HTTPResponse = urllib.request.urlopen(req)
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
|
@ -80,14 +87,6 @@ def fetch_debian_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[s
|
||||||
return debian_url, debian_sha_url
|
return debian_url, debian_sha_url
|
||||||
|
|
||||||
|
|
||||||
def fetch_arch_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
|
||||||
mybase: str = f'{base_url}/jobs/{job_ids["build_archlinux"]}/artifacts/raw'
|
|
||||||
arch_sha_url: str = f'{mybase}/package/archlinux/SHA256SUMS'
|
|
||||||
arch_filename: str = fetch_single_shafile(arch_sha_url)
|
|
||||||
arch_url: str = f'{mybase}/package/archlinux/{arch_filename}'
|
|
||||||
return arch_url, arch_sha_url
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
|
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
|
||||||
release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG')
|
release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG')
|
||||||
|
@ -121,23 +120,39 @@ def main():
|
||||||
|
|
||||||
wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids)
|
wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids)
|
||||||
debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids)
|
debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids)
|
||||||
arch_url, arch_sha_url = fetch_arch_url(base_url, job_ids)
|
|
||||||
|
|
||||||
augmented_changelog = f'''{changelog.strip()}
|
augmented_changelog = f'''{changelog.strip()}
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
- [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url}))
|
- [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url}))
|
||||||
- [Debian Package]({debian_url}) ([sha256]({debian_sha_url}))
|
- [Debian Package]({debian_url}) ([sha256]({debian_sha_url}))'''
|
||||||
- [Arch Linux Package]({arch_url}) ([sha256]({arch_sha_url}))'''
|
|
||||||
|
|
||||||
post_body: str = json.dumps({'description': augmented_changelog})
|
post_body: str = json.dumps({
|
||||||
|
'tag_name': release_tag,
|
||||||
|
'description': augmented_changelog,
|
||||||
|
'assets': {
|
||||||
|
'links': [
|
||||||
|
{
|
||||||
|
'name': 'Python Wheel',
|
||||||
|
'url': wheel_url,
|
||||||
|
'link_type': 'package'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Debian Package',
|
||||||
|
'url': debian_url,
|
||||||
|
'link_type': 'package'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
gitlab_release_api_url: str = \
|
gitlab_release_api_url: str = \
|
||||||
f'https://gitlab.com/api/v4/projects/{project_id}/repository/tags/{release_tag}/release'
|
f'https://gitlab.com/api/v4/projects/{project_id}/releases'
|
||||||
headers: Dict[str, str] = {
|
headers: Dict[str, str] = {
|
||||||
'Private-Token': api_token,
|
'Private-Token': api_token,
|
||||||
'Content-Type': 'application/json; charset=utf-8'
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'User-Agent': 'curl/7.70.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
|
|
11
setup.py
11
setup.py
|
@ -19,8 +19,15 @@ setup(
|
||||||
python_requires='>=3.6',
|
python_requires='>=3.6',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'bottle',
|
'bottle',
|
||||||
'jinja2'
|
'PyYAML',
|
||||||
],
|
],
|
||||||
|
extras_require={
|
||||||
|
'test': [
|
||||||
|
'coverage',
|
||||||
|
'pycodestyle',
|
||||||
|
'twine'
|
||||||
|
]
|
||||||
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'spaceapi-server = spaceapi_server:start'
|
'spaceapi-server = spaceapi_server:start'
|
||||||
|
@ -31,7 +38,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.2'
|
__version__ = '0.5.5'
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -23,12 +23,3 @@ def get() -> dict:
|
||||||
"""
|
"""
|
||||||
global __CONFIG
|
global __CONFIG
|
||||||
return __CONFIG
|
return __CONFIG
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_config(name: str):
|
|
||||||
"""
|
|
||||||
Return a plugin's configuration under .plugins[name]
|
|
||||||
:param name: The plugin name.
|
|
||||||
"""
|
|
||||||
global __CONFIG
|
|
||||||
return __CONFIG.get('plugins', {}).get(name, {})
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
|
|
||||||
import unittest
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from spaceapi_server.config import load, get, 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,5 +1,7 @@
|
||||||
|
|
||||||
from spaceapi_server.template import _env_init
|
import yaml
|
||||||
|
|
||||||
|
from spaceapi_server import config, template
|
||||||
|
|
||||||
|
|
||||||
def template_function(fn):
|
def template_function(fn):
|
||||||
|
@ -7,32 +9,14 @@ 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 = _env_init()
|
template.PluginInvocation.register_plugin(fn.__name__, fn)
|
||||||
# Add the function to the environment's globals
|
|
||||||
env.globals[fn.__name__] = fn
|
|
||||||
return fn
|
return fn
|
||||||
|
|
||||||
|
|
||||||
def template_filter(fn):
|
def get_plugin_config(name: str):
|
||||||
"""
|
"""
|
||||||
Register the decorated function as a template filter.
|
Return a plugin's configuration under .plugins[name]
|
||||||
:param fn: The function to register.
|
:param name: The plugin name.
|
||||||
"""
|
"""
|
||||||
# Make sure the Jinja2 environment is initialized
|
return config.get().get('plugins', {}).get(name, {})
|
||||||
env = _env_init()
|
|
||||||
# Add the function to the environment's filters
|
|
||||||
env.filters[fn.__name__] = fn
|
|
||||||
return fn
|
|
||||||
|
|
||||||
|
|
||||||
def template_test(fn):
|
|
||||||
"""
|
|
||||||
Register the decorated function as a template test.
|
|
||||||
:param fn: The function to register.
|
|
||||||
"""
|
|
||||||
# Make sure the Jinja2 environment is initialized
|
|
||||||
env = _env_init()
|
|
||||||
# Add the function to the environment's tests
|
|
||||||
env.tests[fn.__name__] = 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,78 +0,0 @@
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from spaceapi_server.template import _env_init, render, render_traverse
|
|
||||||
from spaceapi_server.plugins import template_function, template_filter, template_test
|
|
||||||
|
|
||||||
|
|
||||||
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_test
|
|
||||||
def template_test_test(value):
|
|
||||||
return value == 'baz'
|
|
||||||
|
|
||||||
@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") }}',
|
|
||||||
'test_test_true': '{{ "baz" is template_test_test }}',
|
|
||||||
'test_test_false': '{{ "foo" is template_test_test }}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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'])
|
|
||||||
self.assertTrue(rendered['test_functions']['test_test_true'])
|
|
||||||
self.assertFalse(rendered['test_functions']['test_test_false'])
|
|
||||||
|
|
||||||
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