Merge pull request #3 from netzbegruenung/python-service

Python webservice to support digital signage HTML/JS apps
This commit is contained in:
Marian Steinbach 2018-02-15 21:23:03 +01:00 committed by GitHub
commit e046972f9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 295 additions and 1 deletions

View file

@ -30,6 +30,12 @@ kostenlos. Damit eignet sich Yodeck evtl. für kleine Büros.
### Screenly ### Screenly
OpenSource alternative zu Yodeck. Sollte ähnlichen Umfang haben wie Yodeck, aber durch OS flexibler. OpenSource alternative zu Yodeck. Sollte ähnlichen Umfang haben wie Yodeck, aber durch OS flexibler.
Beschreibung folgt. Beschreibung folgt.
https://www.screenly.io/ose/ https://www.screenly.io/ose/
## Software
Im Unterordner `service` entsteht Software zur Erzeugung dynamischer
HTML-Inhalte, die für Digital Signage eingesetzt werden können. Dokumentation
folgt, sobald diese Software einsatzfähig ist.

3
service/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
venv
.pytest_cache
*.pyc

11
service/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM python:3.6-alpine3.6
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
EXPOSE 5000
ENTRYPOINT ["gunicorn", "--bind=0.0.0.0:5000", "api.main:app"]

6
service/Makefile Normal file
View file

@ -0,0 +1,6 @@
serve:
gunicorn --reload -b 0.0.0.0:5000 api.main:app
test:
pytest tests

62
service/README.md Normal file
View file

@ -0,0 +1,62 @@
# Schaufenster Service
Dies ist ein Webservice zur Erzeugung dynamischer Inhalte für
Digital Signage Anwendungen.
_Anwendungsbeispiel:_
Auf einer digitalen Anzeigetafel soll stets aktuell der nächste Sitzungstermin
angezeigt werden. Hierfür geben wir eine iCal-Kalender-URL an
und bekommen dafür Titel und weitere Details der nächsten Termine in diesem
Kalender zurück.
## API
### `GET /events/` - Die nächsten Termine eines iCal Kalenders ausgeben
Request URL Parameter:
- `ical_url`: Adresse des iCal-Kalenders (erforderlich).
- `num`: Maximale Anzahl der Termine, die ausgegeben werden.
- `charset`: Zeichensatz der iCal-Quelle. Normalerweise wird der Zeichensatz
angenommen, den der Webserver im `Content-type` header angibt. Mit diesem
Parameter kann der Wert des Servers überschrieben werden. Beispiel: `charset=utf-8`.
Ausgabe:
```json
[
{
"title": "Karfreitag",
"start": "2018-03-30",
"end": "2018-03-31"
},
...
]
```
Liste mit Terminen als JSON Array. Jeder Termin enthält:
- `title`: Titel des Termins
- `start`: Start-Datum (oder Datum/Uhrzeit) des Termins
- `end`: (optional) Enddatum (oder Datum/Uhrzeit) des Termins
Beispiele:
- [Der nächste Termin beim Grünen Ortsverband Rösrath](https://schaufenster-service.now.sh/events/?charset=utf8&num=1&ical_url=https%3A%2F%2Fgruene-roesrath.de%2Ftermine%2Fcal%2Fics%2F%3Ftype%3D150%26tx_cal_controller%255Bcalendar%255D%3D649)
- [Die nächsten 3 Feiertage in Deutschland](https://schaufenster-service.now.sh/events/?num=3&ical_url=http%3A%2F%2Fwww.webcal.fi%2Fcal.php%3Fid%3D75%26rid%3Dics%26wrn%3D0%26wp%3D12%26wf%3D55)
### Live Demo
Der Service ist erreichbar unter https://schaufenster-service.now.sh/events/
### `GET /luftdaten.info/v1/sensor/{sensor_id}/` - Aktuelle Messwerte eines luftdaten.info Sensors ausgeben
Mit dieser Methode können Feinstaub-Messwerte eines luftdaten.info Sensors
abgerufen werden.
Beispiel:
- https://schaufenster-service.now.sh/luftdateninfo/v1/sensor/6316/

0
service/api/__init__.py Normal file
View file

63
service/api/events.py Normal file
View file

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
import requests
import icalendar
from datetime import datetime
from datetime import date
class Client(object):
def __init__(self, url, charset=None):
self.url = url
self.charset = charset
self.events = []
self.timeout = 20
self.__load()
def __load(self):
r = requests.get(self.url, timeout=self.timeout)
r.raise_for_status()
# requests normally uses encoding returned by "Content-type" header.
# If charset is set, this overwrites the detected character encoding.
if self.charset is not None:
r.encoding = self.charset
cal = icalendar.Calendar.from_ical(r.text)
self.events = []
for event in cal.walk('vevent'):
title = None
if "SUMMARY" in event:
title = event["SUMMARY"]
dtstart = event["DTSTART"].dt
dtend = event["DTEND"].dt
self.events.append({
"title": title,
"start": dtstart,
"end": dtend,
})
# sort events by start datetime
def getdatetime(event):
if isinstance(event["start"], date):
return datetime.combine(event["start"], datetime.min.time())
return event["start"]
self.events = sorted(self.events, key=getdatetime)
def next_events(self, num=10):
"""
Returns the next num events from the calendar
"""
now = datetime.utcnow()
out = []
for event in self.events:
end = event["end"]
if isinstance(end, date):
end = datetime.combine(end, datetime.min.time())
if end > now:
out.append(event)
if len(out) >= num:
break
return out

View file

@ -0,0 +1,38 @@
import six
from datetime import date, datetime
from falcon import errors
from falcon.media import BaseHandler
from falcon.util import json
class ComplexEncoder(json.JSONEncoder):
"""JSONENcoder that handles date and datetime"""
def default(self, obj):
if isinstance(obj, date) or isinstance(obj, datetime):
return obj.isoformat()
# Let the base class default method raise the TypeError
return json.JSONEncoder.default(self, obj)
class JSONHandler(BaseHandler):
"""Handler built using Python's :py:mod:`json` module."""
def deserialize(self, raw):
try:
return json.loads(raw.decode('utf-8'))
except ValueError as err:
raise errors.HTTPBadRequest(
'Invalid JSON',
'Could not parse JSON body - {0}'.format(err)
)
def serialize(self, media):
result = json.dumps(media,
ensure_ascii=False,
cls=ComplexEncoder)
if six.PY3 or not isinstance(result, bytes):
return result.encode('utf-8')
return result

64
service/api/main.py Normal file
View file

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from . import events
from . import jsonhandler
from datetime import datetime
from falcon import media
from falcon_cors import CORS
import falcon
import logging
import requests
class EventsResource(object):
def __init__(self):
self.logger = logging.getLogger('api.' + __name__)
def on_get(self, req, resp):
"""
Loads an ical Calendar and returns the next events
"""
ical_url = req.get_param("ical_url", required=True)
charset = req.get_param("charset")
num = int(req.get_param("num", required=False, default="10"))
client = events.Client(url=ical_url, charset=charset)
next_events = client.next_events(num)
del client
resp.media = next_events
maxage = 60 * 60 # 1 hour
resp.cache_control = ["max_age=%d" % maxage]
class ParticleSensorResource(object):
def on_get(self, req, resp, sensor_id):
"""
Delivers data for a particular luftdaten.info sensor
"""
url = "http://api.luftdaten.info/v1/sensor/%s/" % sensor_id
r = requests.get(url)
if r.status_code == 200:
maxage = 60 * 5 # 5 minutes
resp.cache_control = ["max_age=%d" % maxage]
resp.media = r.json()
else:
resp.media = r.text
resp.status = str(r.status_code) + " Unknown Error"
handlers = media.Handlers({
'application/json': jsonhandler.JSONHandler(),
})
cors = CORS(allow_all_origins=True,
allow_all_headers=True)
app = falcon.API(middleware=[cors.middleware])
app.req_options.media_handlers = handlers
app.resp_options.media_handlers = handlers
app.add_route('/events/', EventsResource())
app.add_route('/luftdaten.info/v1/sensor/{sensor_id}/', ParticleSensorResource())

18
service/requirements.txt Normal file
View file

@ -0,0 +1,18 @@
attrs==17.4.0
certifi==2018.1.18
chardet==3.0.4
falcon==1.4.1
falcon-cors==1.1.7
funcsigs==1.0.2
gunicorn==19.7.1
icalendar==4.0.0
idna==2.6
pluggy==0.6.0
py==1.5.2
pytest==3.4.0
python-dateutil==2.6.1
python-mimeparse==1.6.0
pytz==2017.3
requests==2.18.4
six==1.11.0
urllib3==1.22

View file

View file

@ -0,0 +1,23 @@
import falcon
from falcon import testing
import pytest
from api.main import app
@pytest.fixture
def client():
return testing.TestClient(app)
def test_get_events_no_ical_url(client):
"""
No ical URL given bad request
"""
response = client.simulate_get('/events/')
assert response.status == falcon.HTTP_BAD_REQUEST
# TODO: assertion for response format
def test_get_events(client):
response = client.simulate_get('/events/', params={
"ical_url": "http://www.webcal.fi/cal.php?id=75&rid=ics&wrn=0&wp=12&wf=55"
})
assert response.status == falcon.HTTP_OK