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 ### 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

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 requests
import icalendar import icalendar
@ -7,9 +7,9 @@ from datetime import date
class Client(object): class Client(object):
def __init__(self, url, timezone=None): def __init__(self, url, charset=None):
self.url = url self.url = url
self.timezone = timezone self.charset = charset
self.events = [] self.events = []
self.__load() self.__load()
@ -17,25 +17,33 @@ class Client(object):
r = requests.get(self.url) r = requests.get(self.url)
r.raise_for_status() 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) cal = icalendar.Calendar.from_ical(r.text)
self.events = [] self.events = []
for event in cal.walk('vevent'): for event in cal.walk('vevent'):
title = None title = None
description = None
if "SUMMARY" in event: if "SUMMARY" in event:
title = event["SUMMARY"] title = event["SUMMARY"]
if "DESCRIPTION" in event:
description = event["DESCRIPTION"]
dtstart = event["DTSTART"].dt dtstart = event["DTSTART"].dt
dtend = event["DTEND"].dt dtend = event["DTEND"].dt
self.events.append({ self.events.append({
"title": title, "title": title,
"description": description,
"start": dtstart, "start": dtstart,
"end": dtend, "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): def next_events(self, num=10):
""" """
Returns the next num events from the calendar Returns the next num events from the calendar
@ -48,4 +56,6 @@ class Client(object):
end = datetime.combine(end, datetime.min.time()) end = datetime.combine(end, datetime.min.time())
if end > now: if end > now:
out.append(event) out.append(event)
if len(out) >= num:
break
return out 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