First version of an iCal calender service

This commit is contained in:
Marian Steinbach 2018-02-04 13:20:24 +01:00
parent 99ed046b7a
commit 1fcc52925e
11 changed files with 190 additions and 8 deletions

View File

@ -30,6 +30,12 @@ kostenlos. Damit eignet sich Yodeck evtl. für kleine Büros.
### Screenly
OpenSource alternative zu Yodeck. Sollte ähnlichen Umfang haben wie Yodeck, aber durch OS flexibler.
Beschreibung folgt.
Beschreibung folgt.
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

6
service/Makefile Normal file
View File

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

42
service/README.md Normal file
View File

@ -0,0 +1,42 @@
# 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

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

View File

@ -1,4 +1,4 @@
# coding: utf8
# -*- coding: utf-8 -*-
import requests
import icalendar
@ -7,9 +7,9 @@ from datetime import date
class Client(object):
def __init__(self, url, timezone=None):
def __init__(self, url, charset=None):
self.url = url
self.timezone = timezone
self.charset = charset
self.events = []
self.__load()
@ -17,25 +17,33 @@ class Client(object):
r = requests.get(self.url)
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
description = None
if "SUMMARY" in event:
title = event["SUMMARY"]
if "DESCRIPTION" in event:
description = event["DESCRIPTION"]
dtstart = event["DTSTART"].dt
dtend = event["DTEND"].dt
self.events.append({
"title": title,
"description": description,
"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
@ -48,4 +56,6 @@ class Client(object):
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

37
service/api/main.py Normal file
View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import falcon
from falcon import media
import logging
from . import events
from . import jsonhandler
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)
resp.media = next_events
handlers = media.Handlers({
'application/json': jsonhandler.JSONHandler(),
})
app = falcon.API()
app.req_options.media_handlers = handlers
app.resp_options.media_handlers = handlers
app.add_route('/events/', EventsResource())

17
service/requirements.txt Normal file
View File

@ -0,0 +1,17 @@
attrs==17.4.0
certifi==2018.1.18
chardet==3.0.4
falcon==1.4.1
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