Repository: pimutils/khal
Pull request #717 (2017-10-03 17:28:50)

 Refactor backend and introduce type hinting


Patch:
diff --git a/.gitignore b/.gitignore
index 03aa9f8..a29d4a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ dist/
 .cache
 htmlcov
 doc/source/configspec.rst
+.mypy_cache/
diff --git a/.travis.yml b/.travis.yml
index b264ef5..0b9bd56 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,8 +16,14 @@ matrix:
         env: BUILD=style
       - python: 3.6
         env: BUILD=docs
+      - python: 3.6
+        env: BUILD=mypy
       - python: 3.6
         env: BUILD=pytz201610
+      - python: 3.6
+        env: BUILD=pytz201702
+      - python: 3.6
+        env: BUILD=pytz_latest
 
 addons:
     apt:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b69528f..b180ed4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,8 @@ may want to subscribe to `GitHub's tag feed
 not released yet
 
 * NEW DEPENDENCY added click_log  >= 0.2.0
+* NEW DEPENDENCY for Python 3.4: typing
+* DROPPED support for Python 3.3
 * vdirsyncer is still a test dependency (and always has been)
 
 * FIX ordinal numbers in birthday entries (before, all number would end on `th`)
@@ -27,7 +29,6 @@ not released yet
 * CHANGE `search` will now print one line for every different event in a
   recurrence set, that is one line for the master event, and one line for every
   different overwritten event
-* CHANGE drop support for Python 3.3.
 
 * NEW khal learned the ``--logfile/-l LOGFILE`` flag which allows logging to a
   file
@@ -57,7 +58,7 @@ released 2017-06-13
   present in an .ics file
 * FIX .ics files containing only overwritten instances are not expanded anymore,
   even if they contain a RRULE or RDATE
-* FIX valid UNTIL entry for recurring datetime events 
+* FIX valid UNTIL entry for recurring datetime events
 
 * CHANGE the symbol used for indicating a recurring event now has a space in
   front of it, also the ascii version changed to `(R)`
diff --git a/khal/khalendar/backend.py b/khal/khalendar/backend.py
index b1eb08e..ef1b810 100644
--- a/khal/khalendar/backend.py
+++ b/khal/khalendar/backend.py
@@ -20,29 +20,21 @@
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
 The SQLite backend implementation.
-
-note on naming:
-  * every variable name vevent should be of type icalendar.Event
-  * every variable named event should be of type khal.khalendar.Events
-  * variables named vevents/events (plural) should be iterables of their
-    respective types
-
 """
-# TODO remove creating Events from SQLiteDb
-# we currently expect str/CALENDAR objects but return Event(), we should
-# accept and return the same kind of events
+
 import contextlib
 import datetime as dt
+from enum import IntEnum
 import logging
 import sqlite3
 from os import makedirs, path
+from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
 
 import icalendar
 import pytz
 from dateutil import parser
 
 from .. import utils
-from .event import Event, EventStandIn
 from .exceptions import (CouldNotCreateDbDir, OutdatedDbVersionError,
                          UpdateFailed)
 
@@ -54,29 +46,32 @@ RECURRENCE_ID = 'RECURRENCE-ID'
 THISANDFUTURE = 'THISANDFUTURE'
 THISANDPRIOR = 'THISANDPRIOR'
 
-DATE = 0
-DATETIME = 1
-
 PROTO = 'PROTO'
 
 
+class EventType(IntEnum):
+    DATE = 0
+    DATETIME = 1
+
+
 class SQLiteDb(object):
     """
     This class should provide a caching database for a calendar, keeping raw
-    vevents in one table but allowing to retrieve events by dates (via the help
+    vevents in one table and allowing to retrieve them by dates (via the help
     of some auxiliary tables)
 
-    :param calendar: the `name` of this calendar, if the same *name* and
-                     *dbpath* is given on next creation of an SQLiteDb object
-                     the same tables will be used
-    :type calendar: str
+    :param calendar: names of calendars to select from, those are used as
+        additional itentifiers together with event's uids. Each (uid, calendar)
+        combination should be unique.
     :param db_path: path where this sqlite database will be saved, if this is
-                    None, a place according to the XDG specifications will be
-                    chosen
-    :type db_path: str or None
+        None, a place according to the XDG specifications will be chosen
     """
 
-    def __init__(self, calendars, db_path, locale):
+    def __init__(self,
+                 calendars: Iterable[str],
+                 db_path: Optional[str],
+                 locale: Dict[str, str],
+                 ) -> None:
         assert db_path is not None
         self.calendars = calendars
         self.db_path = path.expanduser(db_path)
@@ -90,7 +85,7 @@ class SQLiteDb(object):
         self._check_table_version()
 
     @property
-    def _select_calendars(self):
+    def _select_calendars(self) -> str:
         return ', '.join(['\'' + cal + '\'' for cal in self.calendars])
 
     @contextlib.contextmanager
@@ -106,7 +101,7 @@ class SQLiteDb(object):
         finally:
             self._at_once = False
 
-    def _create_dbdir(self):
+    def _create_dbdir(self) -> None:
         """create the dbdir if it doesn't exist"""
         if self.db_path == ':memory:':
             return None
@@ -117,10 +112,10 @@ class SQLiteDb(object):
                 makedirs(dbdir, mode=0o770)
                 logger.debug('success')
             except OSError as error:
-                logger.fatal('failed to create {0}: {1}'.format(dbdir, error))
+                logger.critical('failed to create {0}: {1}'.format(dbdir, error))
                 raise CouldNotCreateDbDir()
 
-    def _check_table_version(self):
+    def _check_table_version(self) -> None:
         """tests for current db Version
         if the table is still empty, insert db_version
         """
@@ -136,7 +131,7 @@ class SQLiteDb(object):
                 " is probably an invalid or outdated database.\n"
                 "You should consider removing it and running khal again.")
 
-    def _create_default_tables(self):
+    def _create_default_tables(self) -> None:
         """creates version and calendar tables and inserts table version number
         """
         self.cursor.execute('CREATE TABLE IF NOT EXISTS '
@@ -178,13 +173,12 @@ class SQLiteDb(object):
             );''')
         self.conn.commit()
 
-    def _check_calendars_exists(self):
+    def _check_calendars_exists(self) -> None:
         """make sure an entry for the current calendar exists in `calendar`
         table
         """
         for cal in self.calendars:
-            self.cursor.execute('''SELECT count(*) FROM calendars
-                    WHERE calendar = ?;''', (cal,))
+            self.cursor.execute('''SELECT count(*) FROM calendars WHERE calendar = ?;''', (cal,))
             result = self.cursor.fetchone()
 
             if result[0] != 0:
@@ -194,7 +188,7 @@ class SQLiteDb(object):
                 stuple = (cal, '')
                 self.sql_ex(sql_s, stuple)
 
-    def sql_ex(self, statement, stuple=''):
+    def sql_ex(self, statement=str, stuple: tuple=Union[tuple, str]) -> List:
         """wrapper for sql statements, does a "fetchall" """
         self.cursor.execute(statement, stuple)
         result = self.cursor.fetchall()
@@ -202,28 +196,22 @@ class SQLiteDb(object):
             self.conn.commit()
         return result
 
-    def update(self, vevent_str, href, etag='', calendar=None):
-        """insert a new or update an existing card in the db
+    def update(self, vevent_str: str, href: str, etag: str='', calendar: str=None) -> None:
+        """insert a new or update an existing event into the db
 
         This is mostly a wrapper around two SQL statements, doing some cleanup
         before.
 
         :param vevent_str: event to be inserted or updated.
-                           We assume that even if it contains more than one
-                           VEVENT, that they are all part of the same event and
-                           all have the same UID
-        :type vevent: unicode
+            We assume that even if it contains more than one VEVENT, that they
+            are all part of the same event and all have the same UID
         :param href: href of the card on the server, if this href already
-                     exists in the db the card gets updated. If no href is
-                     given, a random href is chosen and it is implied that this
-                     card does not yet exist on the server, but will be
-                     uploaded there on next sync.
-        :type href: str()
+            exists in the db the card gets updated. If no href is given, a
+            random href is chosen and it is implied that this card does not yet
+            exist on the server, but will be uploaded there on next sync.
         :param etag: the etag of the vcard, if this etag does not match the
-                     remote etag on next sync, this card will be updated from
-                     the server. For locally created vcards this should not be
-                     set
-        :type etag: str()
+            remote etag on next sync, this card will be updated from the server.
+            For locally created vcards this should not be set
         """
         assert calendar is not None
         assert href is not None
@@ -242,20 +230,18 @@ class SQLiteDb(object):
             check_support(vevent, href, calendar)
             self._update_impl(vevent, href, calendar)
 
-        sql_s = ('INSERT INTO events '
-                 '(item, etag, href, calendar) '
-                 'VALUES (?, ?, ?, ?);')
+        sql_s = ('INSERT INTO events (item, etag, href, calendar) VALUES (?, ?, ?, ?);')
         stuple = (vevent_str, etag, href, calendar)
         self.sql_ex(sql_s, stuple)
 
-    def update_birthday(self, vevent, href, etag='', calendar=None):
+    def update_birthday(self, vevent_str: str, href: str, etag: str='', calendar: str=None) -> None:
         """
         XXX write docstring
         """
         assert calendar is not None
         assert href is not None
         self.delete(href, calendar=calendar)
-        ical = icalendar.Event.from_ical(vevent)
+        ical = icalendar.Event.from_ical(vevent_str)
         vcard = ical.walk()[0]
         if 'BDAY' in vcard.keys():
             bday = vcard['BDAY']
@@ -282,26 +268,26 @@ class SQLiteDb(object):
             else:
                 n = vcard['N'].split(';')
                 name = ' '.join([n[1], n[2], n[0]])
-            event = icalendar.Event()
-            event.add('dtstart', bday)
-            event.add('dtend', bday + dt.timedelta(days=1))
+            vevent = icalendar.Event()
+            vevent.add('dtstart', bday)
+            vevent.add('dtend', bday + dt.timedelta(days=1))
             if bday.month == 2 and bday.day == 29:  # leap year
-                event.add('rrule', {'freq': 'YEARLY', 'BYYEARDAY': 60})
+                vevent.add('rrule', {'freq': 'YEARLY', 'BYYEARDAY': 60})
             else:
-                event.add('rrule', {'freq': 'YEARLY'})
+                vevent.add('rrule', {'freq': 'YEARLY'})
             if orig_bday:
-                event.add('x-birthday',
-                          '{:04}{:02}{:02}'.format(bday.year, bday.month, bday.day))
-                event.add('x-fname', name)
-            event.add('summary', '{0}\'s birthday'.format(name))
-            event.add('uid', href)
-            event_str = event.to_ical().decode('utf-8')
-            self._update_impl(event, href, calendar)
+                vevent.add('x-birthday',
+                           '{:04}{:02}{:02}'.format(bday.year, bday.month, bday.day))
+                vevent.add('x-fname', name)
+            vevent.add('summary', '{0}\'s birthday'.format(name))
+            vevent.add('uid', href)
+            vevent_str = vevent.to_ical().decode('utf-8')
+            self._update_impl(vevent, href, calendar)
             sql_s = ('INSERT INTO events (item, etag, href, calendar) VALUES (?, ?, ?, ?);')
-            stuple = (event_str, etag, href, calendar)
+            stuple = (vevent_str, etag, href, calendar)
             self.sql_ex(sql_s, stuple)
 
-    def _update_impl(self, vevent, href, calendar):
+    def _update_impl(self, vevent: icalendar.cal.Event, href: str, calendar: str) -> None:
         """insert `vevent` into the database
 
         expand `vevent`'s recurrence rules (if needed) and insert all instance
@@ -318,10 +304,10 @@ class SQLiteDb(object):
 
         # testing on datetime.date won't work as datetime is a child of date
         if not isinstance(vevent['DTSTART'].dt, dt.datetime):
-            dtype = DATE
+            dtype = EventType.DATE
         else:
-            dtype = DATETIME
-        if ('TZID' in vevent['DTSTART'].params and dtype == DATETIME) or \
+            dtype = EventType.DATETIME
+        if ('TZID' in vevent['DTSTART'].params and dtype == EventType.DATETIME) or \
                 getattr(vevent['DTSTART'].dt, 'tzinfo', None):
             recs_table = 'recs_loc'
         else:
@@ -330,8 +316,8 @@ class SQLiteDb(object):
         thisandfuture = (rrange == THISANDFUTURE)
         if thisandfuture:
             start_shift, duration = calc_shift_deltas(vevent)
-            start_shift = start_shift.days * 3600 * 24 + start_shift.seconds
-            duration = duration.days * 3600 * 24 + duration.seconds
+            start_shift_seconds = start_shift.days * 3600 * 24 + start_shift.seconds
+            duration_seconds = duration.days * 3600 * 24 + duration.seconds
 
         dtstartend = utils.expand(vevent, href)
         if not dtstartend:
@@ -341,7 +327,7 @@ class SQLiteDb(object):
             return
 
         for dtstart, dtend in dtstartend:
-            if dtype == DATE:
+            if dtype == EventType.DATE:
                 dbstart = utils.to_unix_time(dtstart)
                 dbend = utils.to_unix_time(dtend)
             else:
@@ -358,7 +344,10 @@ class SQLiteDb(object):
                 recs_sql_s = (
                     'UPDATE {0} SET dtstart = rec_inst + ?, dtend = rec_inst + ?, ref = ? '
                     'WHERE rec_inst >= ? AND href = ? AND calendar = ?;'.format(recs_table))
-                stuple = (start_shift, start_shift + duration, ref, rec_inst, href, calendar)
+                stuple = (
+                    start_shift_seconds, start_shift_seconds + duration_seconds,
+                    ref, rec_inst, href, calendar,
+                )
             else:
                 recs_sql_s = (
                     'INSERT OR REPLACE INTO {0} '
@@ -367,7 +356,7 @@ class SQLiteDb(object):
                 stuple = (dbstart, dbend, href, ref, dtype, rec_inst, calendar)
             self.sql_ex(recs_sql_s, stuple)
 
-    def get_ctag(self, calendar):
+    def get_ctag(self, calendar=str) -> Optional[str]:
         stuple = (calendar, )
         sql_s = 'SELECT ctag FROM calendars WHERE calendar = ?;'
         try:
@@ -376,13 +365,13 @@ class SQLiteDb(object):
         except IndexError:
             return None
 
-    def set_ctag(self, ctag, calendar):
+    def set_ctag(self, ctag: str, calendar: str):
         stuple = (ctag, calendar, )
         sql_s = 'UPDATE calendars SET ctag = ? WHERE calendar = ?;'
         self.sql_ex(sql_s, stuple)
         self.conn.commit()
 
-    def get_etag(self, href, calendar):
+    def get_etag(self, href: str, calendar: str) -> Optional[str]:
         """get etag for href
 
         type href: str()
@@ -396,7 +385,7 @@ class SQLiteDb(object):
         except IndexError:
             return None
 
-    def delete(self, href, etag=None, calendar=None):
+    def delete(self, href: str, etag: Any=None, calendar: str=None):
         """
         removes the event from the db,
 
@@ -420,7 +409,27 @@ class SQLiteDb(object):
         sql_s = 'SELECT href, etag FROM events WHERE calendar = ?;'
         return list(set(self.sql_ex(sql_s, (calendar, ))))
 
-    def get_localized(self, start, end, minimal=False):
+    def get_localized_calendars(self, start: dt.datetime, end: dt.datetime) -> Iterable[str]:
+        assert start.tzinfo is not None
+        assert end.tzinfo is not None
+        start = utils.to_unix_time(start)
+        end = utils.to_unix_time(end)
+        sql_s = (
+            'SELECT events.calendar FROM '
+            'recs_loc JOIN events ON '
+            'recs_loc.href = events.href AND '
+            'recs_loc.calendar = events.calendar WHERE '
+            '(dtstart >= ? AND dtstart <= ? OR '
+            'dtend > ? AND dtend <= ? OR '
+            'dtstart <= ? AND dtend >= ?) AND events.calendar in ({0}) '
+            'ORDER BY dtstart')
+        stuple = (start, end, start, end, start, end)
+        result = self.sql_ex(sql_s.format(self._select_calendars), stuple)
+        for calendar in result:
+            yield calendar[0]  # result is always an iterable, even if getting only one item
+
+    def get_localized(self, start, end) \
+            -> Iterable[Tuple[str, str, dt.datetime, dt.datetime, str, str, str]]:
         """returns
         :type start: datetime.datetime
         :type end: datetime.datetime
@@ -431,118 +440,80 @@ class SQLiteDb(object):
         assert end.tzinfo is not None
         start = utils.to_unix_time(start)
         end = utils.to_unix_time(end)
-        if minimal:
-            sql_s = (
-                'SELECT events.calendar FROM '
-                'recs_loc JOIN events ON '
-                'recs_loc.href = events.href AND '
-                'recs_loc.calendar = events.calendar WHERE '
-                '(dtstart >= ? AND dtstart <= ? OR '
-                'dtend > ? AND dtend <= ? OR '
-                'dtstart <= ? AND dtend >= ?) AND events.calendar in ({0}) '
-                'ORDER BY dtstart')
-        else:
-            sql_s = (
-                'SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar '
-                'FROM recs_loc JOIN events ON '
-                'recs_loc.href = events.href AND '
-                'recs_loc.calendar = events.calendar WHERE '
-                '(dtstart >= ? AND dtstart <= ? OR '
-                'dtend > ? AND dtend <= ? OR '
-                'dtstart <= ? AND dtend >= ?) AND events.calendar in ({0}) '
-                'ORDER BY dtstart')
+        sql_s = (
+            'SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar '
+            'FROM recs_loc JOIN events ON '
+            'recs_loc.href = events.href AND '
+            'recs_loc.calendar = events.calendar WHERE '
+            '(dtstart >= ? AND dtstart <= ? OR '
+            'dtend > ? AND dtend <= ? OR '
+            'dtstart <= ? AND dtend >= ?) AND events.calendar in ({0}) '
+            'ORDER BY dtstart')
         stuple = (start, end, start, end, start, end)
         result = self.sql_ex(sql_s.format(self._select_calendars), stuple)
-        if minimal:
-            for calendar in result:
-                yield EventStandIn(calendar[0])
-        else:
-            for item, href, start, end, ref, etag, dtype, calendar in result:
-                start = pytz.UTC.localize(dt.datetime.utcfromtimestamp(start))
-                end = pytz.UTC.localize(dt.datetime.utcfromtimestamp(end))
-                yield self.construct_event(item, href, start, end, ref, etag, calendar, dtype)
+        for item, href, start, end, ref, etag, dtype, calendar in result:
+            start = pytz.UTC.localize(dt.datetime.utcfromtimestamp(start))
+            end = pytz.UTC.localize(dt.datetime.utcfromtimestamp(end))
+            yield item, href, start, end, ref, etag, calendar
 
-    def get_floating(self, start, end, minimal=False):
+    def get_floating_calendars(self, start: dt.datetime, end: dt.datetime) -> Iterable[str]:
+        assert start.tzinfo is None
+        assert end.tzinfo is None
+        strstart = utils.to_unix_time(start)
+        strend = utils.to_unix_time(end)
+        sql_s = (
+            'SELECT events.calendar FROM '
+            'recs_float JOIN events ON '
+            'recs_float.href = events.href AND '
+            'recs_float.calendar = events.calendar WHERE '
+            '(dtstart >= ? AND dtstart < ? OR '
+            'dtend > ? AND dtend <= ? OR '
+            'dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) '
+            'ORDER BY dtstart')
+        stuple = (strstart, strend, strstart, strend, strstart, strend)
+        result = self.sql_ex(sql_s.format(self._select_calendars), stuple)
+        for calendar in result:
+            yield calendar[0]
+
+    def get_floating(self, start, end) \
+            -> Iterable[Tuple[str, str, dt.datetime, dt.datetime, str, str, str]]:
         """return floating events between `start` and `end`
 
         :type start: datetime.datetime
         :type end: datetime.datetime
-        :param minimal: if set, we do not return an event but a minimal stand in
-        :type minimal: bool
         """
         assert start.tzinfo is None
         assert end.tzinfo is None
         strstart = utils.to_unix_time(start)
         strend = utils.to_unix_time(end)
-        if minimal:
-            sql_s = (
-                'SELECT events.calendar FROM '
-                'recs_float JOIN events ON '
-                'recs_float.href = events.href AND '
-                'recs_float.calendar = events.calendar WHERE '
-                '(dtstart >= ? AND dtstart < ? OR '
-                'dtend > ? AND dtend <= ? OR '
-                'dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) '
-                'ORDER BY dtstart')
-        else:
-            sql_s = (
-                'SELECT item, recs_float.href, dtstart, dtend, ref, etag, dtype, events.calendar '
-                'FROM recs_float JOIN events ON '
-                'recs_float.href = events.href AND '
-                'recs_float.calendar = events.calendar WHERE '
-                '(dtstart >= ? AND dtstart < ? OR '
-                'dtend > ? AND dtend <= ? OR '
-                'dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) '
-                'ORDER BY dtstart')
+        sql_s = (
+            'SELECT item, recs_float.href, dtstart, dtend, ref, etag, dtype, events.calendar '
+            'FROM recs_float JOIN events ON '
+            'recs_float.href = events.href AND '
+            'recs_float.calendar = events.calendar WHERE '
+            '(dtstart >= ? AND dtstart < ? OR '
+            'dtend > ? AND dtend <= ? OR '
+            'dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) '
+            'ORDER BY dtstart')
         stuple = (strstart, strend, strstart, strend, strstart, strend)
         result = self.sql_ex(sql_s.format(self._select_calendars), stuple)
-        if minimal:
-            for calendar in result:
-                yield EventStandIn(calendar[0])
-        else:
-            for item, href, start, end, ref, etag, dtype, calendar in result:
-                start = dt.datetime.utcfromtimestamp(start)
-                end = dt.datetime.utcfromtimestamp(end)
-                yield self.construct_event(item, href, start, end, ref, etag, calendar, dtype)
-
-    def get(self, href, start=None, end=None, ref=None, dtype=None, calendar=None):
-        """returns the Event matching href
+        for item, href, start, end, ref, etag, dtype, calendar in result:
+            start = dt.datetime.utcfromtimestamp(start)
+            end = dt.datetime.utcfromtimestamp(end)
+            if dtype == EventType.DATE:
+                start = start.date()
+                end = end.date()
+            yield item, href, start, end, ref, etag, calendar
 
-        if start and end are given, a specific Event from a Recursion set is
-        returned, otherwise the Event returned exactly as saved in the db
-        """
+    def get(self, href: str, calendar: str) -> str:
+        """returns the ical string matching href and calendar"""
         assert calendar is not None
-        sql_s = 'SELECT href, etag, item FROM events WHERE href = ? AND calendar = ?;'
-        result = self.sql_ex(sql_s, (href, calendar))
-        href, etag, item = result[0]
-        if dtype == DATE:
-            start = start.date()
-            end = end.date()
-        return Event.fromString(item,
-                                locale=self.locale,
-                                href=href,
-                                calendar=calendar,
-                                etag=etag,
-                                start=start,
-                                end=end,
-                                ref=ref,
-                                )
-
-    def construct_event(self, item, href, start, end, ref, etag, calendar, dtype=None):
-        if dtype == DATE:
-            start = start.date()
-            end = end.date()
-        return Event.fromString(item,
-                                locale=self.locale,
-                                href=href,
-                                calendar=calendar,
-                                etag=etag,
-                                start=start,
-                                end=end,
-                                ref=ref,
-                                )
-
-    def search(self, search_string):
+        sql_s = 'SELECT item, etag FROM events WHERE href = ? AND calendar = ?;'
+        item, etag = self.sql_ex(sql_s, (href, calendar))[0]
+        return item
+
+    def search(self, search_string: str) \
+            -> Iterable[Tuple[str, str, dt.datetime, dt.datetime, str, str, str]]:
         """search for events matching `search_string`"""
         sql_s = (
             'SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar '
@@ -556,7 +527,10 @@ class SQLiteDb(object):
         for item, href, start, end, ref, etag, dtype, calendar in result:
             start = pytz.UTC.localize(dt.datetime.utcfromtimestamp(start))
             end = pytz.UTC.localize(dt.datetime.utcfromtimestamp(end))
-            yield self.construct_event(item, href, start, end, ref, etag, calendar, dtype)
+            if dtype == EventType.DATE:
+                start = start.date()
+                end = end.date()
+            yield item, href, start, end, ref, etag, calendar
 
         sql_s = (
             'SELECT item, recs_float.href, dtstart, dtend, ref, etag, dtype, events.calendar '
@@ -570,16 +544,17 @@ class SQLiteDb(object):
         for item, href, start, end, ref, etag, dtype, calendar in result:
             start = dt.datetime.utcfromtimestamp(start)
             end = dt.datetime.utcfromtimestamp(end)
-            yield self.construct_event(item, href, start, end, ref, etag, calendar, dtype)
+            if dtype == EventType.DATE:
+                start = start.date()
+                end = end.date()
+            yield item, href, start, end, ref, etag, calendar
 
 
-def check_support(vevent, href, calendar):
+def check_support(vevent: icalendar.cal.Event, href: str, calendar: str):
     """test if all icalendar features used in this event are supported,
     raise `UpdateFailed` otherwise.
     :param vevent: event to test
-    :type vevent: icalendar.cal.Event
     :param href: href of this event, only used for logging
-    :type href: str
     """
     rec_id = vevent.get(RECURRENCE_ID)
 
@@ -602,7 +577,7 @@ def check_support(vevent, href, calendar):
         )
 
 
-def check_for_errors(component, calendar, href):
+def check_for_errors(component: icalendar.cal.Component, calendar: str, href: str):
     """checking if component.errors exists, is not empty and if so warn the user"""
     if hasattr(component, 'errors') and component.errors:
         logger.error(
@@ -613,14 +588,11 @@ def check_for_errors(component, calendar, href):
         logger.error('This might lead to this event being shown wrongly or not at all.')
 
 
-def calc_shift_deltas(vevent):
+def calc_shift_deltas(vevent: icalendar.Event) -> Tuple[dt.timedelta, dt.timedelta]:
     """calculate an event's duration and by how much its start time has shifted
     versus its recurrence-id time
 
     :param event: an event with an RECURRENCE-ID property
-    :type event: icalendar.Event
-    :returns: time shift and duration
-    :rtype: (datetime.timedelta, datetime.timedelta)
     """
     assert isinstance(vevent, icalendar.Event)  # REMOVE ME
     start_shift = vevent['DTSTART'].dt - vevent['RECURRENCE-ID'].dt
diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py
index 7ebd133..8bc7aef 100644
--- a/khal/khalendar/event.py
+++ b/khal/khalendar/event.py
@@ -69,6 +69,7 @@ class Event(object):
         self.href = kwargs.pop('href', None)
         self.etag = kwargs.pop('etag', None)
         self.calendar = kwargs.pop('calendar', None)
+        self.color = kwargs.pop('color', None)
         self.ref = ref
 
         start = kwargs.pop('start', None)
@@ -846,11 +847,3 @@ def _create_timezone_static(tz):
     subcomp.add('TZOFFSETFROM', tz._utcoffset)
     timezone.add_component(subcomp)
     return timezone
-
-
-class EventStandIn():
-    def __init__(self, calendar):
-        self.calendar = calendar
-        self.color = None
-        self.unicode_symbols = None
-        self.readonly = None
diff --git a/khal/khalendar/exceptions.py b/khal/khalendar/exceptions.py
index 2240e62..7ce179d 100644
--- a/khal/khalendar/exceptions.py
+++ b/khal/khalendar/exceptions.py
@@ -19,6 +19,8 @@
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
+from typing import Optional  # noqa
+
 from ..exceptions import Error, FatalError, UnsupportedFeatureError
 
 
@@ -57,4 +59,4 @@ class UpdateFailed(Error):
 class DuplicateUid(Error):
 
     """an event with this UID already exists"""
-    existing_href = None
+    existing_href = None  # type: Optional[str]
diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py
index 896bb4f..60b97bb 100644
--- a/khal/khalendar/khalendar.py
+++ b/khal/khalendar/khalendar.py
@@ -30,6 +30,7 @@ import itertools
 import logging
 import os
 import os.path
+from typing import Dict, Iterable, List, Optional, Tuple, Union  # noqa
 
 from . import backend
 from .event import Event
@@ -42,14 +43,14 @@ from .vdir import (AlreadyExistingError, CollectionNotFoundError, Vdir,
 logger = logging.getLogger(__name__)
 
 
-def create_directory(path):
+def create_directory(path: str):
     if not os.path.isdir(path):
         if os.path.exists(path):
             raise RuntimeError('{0} is not a directory.'.format(path))
         try:
             os.makedirs(path, mode=0o750)
         except OSError as error:
-            logger.fatal('failed to create {0}: {1}'.format(path, error))
+            logger.critical('failed to create {0}: {1}'.format(path, error))
             raise CouldNotCreateDbDir()
 
 
@@ -60,19 +61,19 @@ class CalendarCollection(object):
 
     def __init__(self,
                  calendars=None,
-                 hmethod='fg',
-                 default_color='',
-                 multiple='',
-                 color='',
-                 highlight_event_days=0,
-                 locale=None,
-                 dbpath=None,
-                 ):
+                 hmethod: str='fg',
+                 default_color: str='',
+                 multiple: str='',
+                 color: str='',
+                 highlight_event_days: bool=False,
+                 locale: Optional[dict]=None,
+                 dbpath: Optional[str]=None,
+                 ) -> None:
         assert dbpath is not None
         assert calendars is not None
         self._calendars = calendars
-        self._default_calendar_name = None
-        self._storages = dict()
+        self._default_calendar_name = None  # type: Optional[str]
+        self._storages = dict()  # type: Dict[str, Vdir]
         for name, calendar in self._calendars.items():
             ctype = calendar.get('ctype', 'calendar')
             if ctype == 'calendar':
@@ -94,29 +95,28 @@ class CalendarCollection(object):
         self.color = color
         self.highlight_event_days = highlight_event_days
         self._locale = locale
-        self._backend = backend.SQLiteDb(
-            calendars=self.names, db_path=dbpath, locale=self._locale)
-        self._last_ctags = dict()
+        self._backend = backend.SQLiteDb(self.names, dbpath, self._locale)
+        self._last_ctags = dict()  # type: Dict[str, str]
         self.update_db()
 
     @property
-    def writable_names(self):
+    def writable_names(self) -> List[str]:
         return [c for c in self._calendars if not self._calendars[c].get('readonly', False)]
 
     @property
-    def calendars(self):
+    def calendars(self) -> Iterable[str]:
         return self._calendars.values()
 
     @property
-    def names(self):
+    def names(self) -> Iterable[str]:
         return self._calendars.keys()
 
     @property
-    def default_calendar_name(self):
+    def default_calendar_name(self) -> str:
         return self._default_calendar_name
 
     @default_calendar_name.setter
-    def default_calendar_name(self, default):
+    def default_calendar_name(self, default: str):
         if default is None:
             self._default_calendar_name = default
         elif default not in self.names:
@@ -130,38 +130,37 @@ class CalendarCollection(object):
             raise ValueError(
                 'Calendar "{0}" is read-only and cannot be used as default'.format(default))
 
-    def _local_ctag(self, calendar):
+    def _local_ctag(self, calendar: str) -> str:
         return get_etag_from_file(self._calendars[calendar]['path'])
 
-    def _cover_event(self, event):
-        event.color = self._calendars[event.calendar]['color']
-        event.readonly = self._calendars[event.calendar]['readonly']
-        event.unicode_symbols = self._locale['unicode_symbols']
-        return event
-
-    def get_floating(self, start, end, minimal=False):
-        events = self._backend.get_floating(start, end, minimal)
-        return (self._cover_event(event) for event in events)
-
-    def get_localized(self, start, end, minimal=False):
-        events = self._backend.get_localized(start, end, minimal)
-        return (self._cover_event(event) for event in events)
+    def get_floating(self, start: dt.datetime, end: dt.datetime) -> Iterable[Event]:
+        for args in self._backend.get_floating(start, end):
+            yield self._construct_event(*args)
 
-    def get_events_on(self, day, minimal=False):
-        """return all events on `day`
+    def get_localized(self, start: dt.datetime, end: dt.datetime) -> Iterable[Event]:
+        for args in self._backend.get_localized(start, end):
+            yield self._construct_event(*args)
 
-        :param day: datetime.date
-        :rtype: list()
-        """
+    def get_events_on(self, day: dt.date) -> Iterable[Event]:
+        """return all events on `day`"""
         start = dt.datetime.combine(day, dt.time.min)
         end = dt.datetime.combine(day, dt.time.max)
-        floating_events = self.get_floating(start, end, minimal)
+        floating_events = self.get_floating(start, end)
         localize = self._locale['local_timezone'].localize
-        localized_events = self.get_localized(localize(start), localize(end), minimal)
-
+        localized_events = self.get_localized(localize(start), localize(end))
         return itertools.chain(floating_events, localized_events)
 
-    def update(self, event):
+    def get_calendars_on(self, day: dt.date) -> List[str]:
+        start = dt.datetime.combine(day, dt.time.min)
+        end = dt.datetime.combine(day, dt.time.max)
+        localize = self._locale['local_timezone'].localize
+        calendars = itertools.chain(
+            self._backend.get_floating_calendars(start, end),
+            self._backend.get_localized_calendars(localize(start), localize(end)),
+        )
+        return list(calendars)
+
+    def update(self, event: Event):
         """update `event` in vdir and db"""
         assert event.etag
         if self._calendars[event.calendar]['readonly']:
@@ -171,7 +170,7 @@ class CalendarCollection(object):
             self._backend.update(event.raw, event.href, event.etag, calendar=event.calendar)
             self._backend.set_ctag(self._local_ctag(event.calendar), calendar=event.calendar)
 
-    def force_update(self, event, collection=None):
+    def force_update(self, event: Event, collection: Optional[str]=None):
         """update `event` even if an event with the same uid/href already exists"""
         calendar = collection if collection is not None else event.calendar
         if self._calendars[calendar]['readonly']:
@@ -187,7 +186,7 @@ class CalendarCollection(object):
             self._backend.update(event.raw, href, etag, calendar=calendar)
             self._backend.set_ctag(self._local_ctag(calendar), calendar=calendar)
 
-    def new(self, event, collection=None):
+    def new(self, event: Event, collection: Optional[str]=None):
         """save a new event to the vdir and the database
 
         param event: the event that should be updated, will get a new href and
@@ -210,22 +209,48 @@ class CalendarCollection(object):
             self._backend.update(event.raw, event.href, event.etag, calendar=calendar)
             self._backend.set_ctag(self._local_ctag(calendar), calendar=calendar)
 
-    def delete(self, href, etag, calendar):
+    def delete(self, href: str, etag: str, calendar: str):
         if self._calendars[calendar]['readonly']:
             raise ReadOnlyCalendarError()
         self._storages[calendar].delete(href, etag)
         self._backend.delete(href, calendar=calendar)
 
-    def get_event(self, href, calendar):
-        return self._cover_event(self._backend.get(href, calendar=calendar))
+    def get_event(self, href: str, calendar: str) -> Event:
+        """get an event by its href from the datatbase"""
+        return self._construct_event(
+            self._backend.get(href, calendar), href=href, calendar=calendar,
+        )
+
+    def _construct_event(self,
+                         item: str,
+                         href: str,
+                         start: dt.datetime = None,
+                         end: dt.datetime = None,
+                         ref: str='PROTO',
+                         etag: str=None,
+                         calendar: str=None,
+                         ) -> Event:
+        event = Event.fromString(
+            item,
+            locale=self._locale,
+            href=href,
+            calendar=calendar,
+            etag=etag,
+            start=start,
+            end=end,
+            ref=ref,
+            color=self._calendars[calendar]['color'],
+            readonly=self._calendars[calendar]['readonly'],
+        )
+        return event
 
-    def change_collection(self, event, new_collection):
+    def change_collection(self, event: Event, new_collection: str):
         href, etag, calendar = event.href, event.etag, event.calendar
         event.etag = None
         self.new(event, new_collection)
         self.delete(href, etag, calendar=calendar)
 
-    def new_event(self, ical, collection):
+    def new_event(self, ical: str, collection: str):
         """creates and returns (but does not insert) new event from ical
         string"""
         calendar = collection or self.writable_names[0]
@@ -240,7 +265,7 @@ class CalendarCollection(object):
             if self._needs_update(calendar, remember=True):
                 self._db_update(calendar)
 
-    def needs_update(self):
+    def needs_update(self) -> bool:
         """Check if you need to call update_db.
 
         This could either be the case because the vdirs were changed externally,
@@ -266,14 +291,14 @@ class CalendarCollection(object):
                 return True
         return False
 
-    def _needs_update(self, calendar, remember=False):
+    def _needs_update(self, calendar: str, remember: bool=False) -> bool:
         """checks if the db for the given calendar needs an update"""
         local_ctag = self._local_ctag(calendar)
         if remember:
             self._last_ctags[calendar] = local_ctag
         return local_ctag != self._backend.get_ctag(calendar)
 
-    def _db_update(self, calendar):
+    def _db_update(self, calendar: str):
         """implements the actual db update on a per calendar base"""
         local_ctag = self._local_ctag(calendar)
         db_hrefs = set(href for href, etag in self._backend.list(calendar))
@@ -291,7 +316,7 @@ class CalendarCollection(object):
             self._backend.set_ctag(local_ctag, calendar=calendar)
             self._last_ctags[calendar] = local_ctag
 
-    def _update_vevent(self, href, calendar):
+    def _update_vevent(self, href: str, calendar: str) -> bool:
         """should only be called during db_update, only updates the db,
         does not check for readonly"""
         event, etag = self._storages[calendar].get(href)
@@ -301,7 +326,6 @@ class CalendarCollection(object):
             else:
                 update = self._backend.update
             update(event.raw, href=href, etag=etag, calendar=calendar)
-
             return True
         except Exception as e:
             if not isinstance(e, (UpdateFailed, UnsupportedFeatureError)):
@@ -311,24 +335,23 @@ class CalendarCollection(object):
                 'This event will not be available in khal.'.format(calendar, href, str(e)))
             return False
 
-    def search(self, search_string):
+    def search(self, search_string: str) -> Iterable[Event]:
         """search for the db for events matching `search_string`"""
-        return (self._cover_event(event) for event in self._backend.search(search_string))
+        return (self._construct_event(*args) for args in self._backend.search(search_string))
 
-    def get_day_styles(self, day, focus):
-        devents = list(self.get_events_on(day, minimal=True))
-        if len(devents) == 0:
+    def get_day_styles(self, day: dt.date, focus: bool) -> Union[str, Tuple[str, str]]:
+        calendars = self.get_calendars_on(day)
+        if len(calendars) == 0:
             return None
         if self.color != '':
             return 'highlight_days_color'
-        dcalendars = list(set(map(lambda event: event.calendar, devents)))
-        if len(dcalendars) == 1:
-            return 'calendar ' + dcalendars[0]
+        if len(calendars) == 1:
+            return 'calendar ' + calendars[0]
         if self.multiple != '':
             return 'highlight_days_multiple'
-        return ('calendar ' + dcalendars[0], 'calendar ' + dcalendars[1])
+        return ('calendar ' + calendars[0], 'calendar ' + calendars[1])
 
-    def get_styles(self, date, focus):
+    def get_styles(self, date: dt.date, focus: bool) -> Union[str, None, Tuple[str, str]]:
         if focus:
             if date == date.today():
                 return 'today focus'
diff --git a/khal/khalendar/vdir.py b/khal/khalendar/vdir.py
index 367e247..467566e 100644
--- a/khal/khalendar/vdir.py
+++ b/khal/khalendar/vdir.py
@@ -7,6 +7,8 @@ import errno
 import os
 import uuid
 
+from typing import Optional  # noqa
+
 from atomicwrites import atomic_write
 
 
@@ -111,7 +113,7 @@ class WrongEtagError(VdirError):
 
 
 class AlreadyExistingError(VdirError):
-    existing_href = None
+    existing_href = None  # type: Optional[str]
 
 
 class Item:
diff --git a/khal/settings/settings.py b/khal/settings/settings.py
index e429cce..0dbfe88 100644
--- a/khal/settings/settings.py
+++ b/khal/settings/settings.py
@@ -116,19 +116,19 @@ def get_config(
     results = user_config.validate(validator, preserve_errors=True)
 
     abort = False
-    for section, subsection, error in flatten_errors(user_config, results):
+    for section, subsection, config_error in flatten_errors(user_config, results):
         abort = True
-        if isinstance(error, Exception):
+        if isinstance(config_error, Exception):
             logger.fatal(
                 'config error:\n'
-                'in [{}] {}: {}'.format(section[0], subsection, error))
+                'in [{}] {}: {}'.format(section[0], subsection, config_error))
         else:
-            for key in error:
-                if isinstance(error[key], Exception):
+            for key in config_error:
+                if isinstance(config_error[key], Exception):
                     logger.fatal('config error:\nin {} {}: {}'.format(
                         sectionize(section + [subsection]),
                         key,
-                        str(error[key]))
+                        str(config_error[key]))
                     )
 
     if abort or not results:
diff --git a/setup.py b/setup.py
index e6a3a93..622cc25 100755
--- a/setup.py
+++ b/setup.py
@@ -31,6 +31,7 @@ test_requirements = [
 
 extra_requirements = {
     'proctitle': ['setproctitle'],
+    ':python_version < "3.5"': 'typing',
 }
 
 setup(
diff --git a/tox.ini b/tox.ini
index d67b293..3cc0459 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = {py34,py35,py36}-{tests,style}-{pytz201702,pytz201610}
+envlist = {py34,py35,py36}-{tests,style,mypy}-{pytz201702,pytz201610,pytz_latest}
 skip_missing_interpreters = True
 
 [testenv]
@@ -23,6 +23,8 @@ deps =
     vdirsyncer
     pytz201702: pytz==2017.2
     pytz201610: pytz==2016.10
+    pytz_latest: pytz
+    py34: typing
 
 commands =
     py.test --cov khal {posargs}
@@ -43,6 +45,13 @@ commands =
   make -C doc html
   make -C doc man
 
+[testenv:mypy]
+skip_install=True
+whitelist_externals = sh
+deps = mypy
+commands =
+    mypy --ignore-missing-imports khal
+
 [flake8]
 max-line-length = 100
 exclude=.tox,examples,doc


Test patch:
diff --git a/tests/backend_test.py b/tests/backend_test.py
index e1ad0cb..46bcea8 100644
--- a/tests/backend_test.py
+++ b/tests/backend_test.py
@@ -1,15 +1,14 @@
 
 import datetime as dt
+from operator import itemgetter
 
 import icalendar
 import pkg_resources
 import pytest
 from khal.khalendar import backend
-from khal.khalendar.event import EventStandIn, LocalizedEvent
 from khal.khalendar.exceptions import OutdatedDbVersionError, UpdateFailed
 
-from .utils import (BERLIN, LOCALE_BERLIN, LOCALE_SYDNEY, LONDON, SYDNEY,
-                    _get_text)
+from .utils import BERLIN, LOCALE_BERLIN, _get_text
 
 calname = 'home'
 
@@ -35,90 +34,41 @@ def test_event_rrule_recurrence_id():
         BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
         BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)),
     )
-    events = sorted(events, key=lambda x: x.start)
+    events = sorted(events, key=itemgetter(2))
     assert len(events) == 6
 
-    assert events[0].start == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
-    assert events[1].start == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
-    assert events[2].start == BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0))
-    assert events[3].start == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
-    assert events[4].start == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
-    assert events[5].start == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
+    # start
+    assert events[0][2] == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
+    assert events[1][2] == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
+    assert events[2][2] == BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0))
+    assert events[3][2] == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
+    assert events[4][2] == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
+    assert events[5][2] == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
 
-    events = dbi.get_localized(
+    calendars = dbi.get_localized_calendars(
         BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
         BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)),
-        minimal=True,
-    )
-    events = list(events)
-    assert len(events) == 6
-    for event in events:
-        assert isinstance(event, EventStandIn)
-
-
-def test_event_different_timezones():
-    dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN)
-    dbi.update(_get_text('event_dt_london'), href='12345.ics', etag='abcd', calendar=calname)
-
-    events = dbi.get_localized(
-        BERLIN.localize(dt.datetime(2014, 4, 9, 0, 0)),
-        BERLIN.localize(dt.datetime(2014, 4, 9, 23, 59)),
-    )
-    events = list(events)
-    assert len(events) == 1
-    event = events[0]
-    assert event.start_local == LONDON.localize(dt.datetime(2014, 4, 9, 14))
-    assert event.end_local == LONDON.localize(dt.datetime(2014, 4, 9, 19))
-    assert event.start == LONDON.localize(dt.datetime(2014, 4, 9, 14))
-    assert event.end == LONDON.localize(dt.datetime(2014, 4, 9, 19))
-
-    # no event scheduled on the next day
-    events = dbi.get_localized(
-        BERLIN.localize(dt.datetime(2014, 4, 10, 0, 0)),
-        BERLIN.localize(dt.datetime(2014, 4, 10, 23, 59)),
     )
-    events = list(events)
-    assert len(events) == 0
-
-    # now setting the local_timezone to Sydney
-    dbi.locale = LOCALE_SYDNEY
-    events = dbi.get_localized(
-        SYDNEY.localize(dt.datetime(2014, 4, 9, 0, 0)),
-        SYDNEY.localize(dt.datetime(2014, 4, 9, 23, 59)),
-    )
-    events = list(events)
-    assert len(events) == 1
-    event = events[0]
-    assert event.start_local == SYDNEY.localize(dt.datetime(2014, 4, 9, 23))
-    assert event.end_local == SYDNEY.localize(dt.datetime(2014, 4, 10, 4))
-    assert event.start == LONDON.localize(dt.datetime(2014, 4, 9, 14))
-    assert event.end == LONDON.localize(dt.datetime(2014, 4, 9, 19))
-
-    # the event spans midnight Sydney, therefor it should also show up on the
-    # next day
-    events = dbi.get_localized(SYDNEY.localize(dt.datetime(2014, 4, 10, 0, 0)),
-                               SYDNEY.localize(dt.datetime(2014, 4, 10, 23, 59)))
-    events = list(events)
-    assert len(events) == 1
-    assert event.start_local == SYDNEY.localize(dt.datetime(2014, 4, 9, 23))
-    assert event.end_local == SYDNEY.localize(dt.datetime(2014, 4, 10, 4))
+    calendars = list(calendars)
+    assert len(calendars) == 6
 
 
 def test_event_rrule_recurrence_id_invalid_tzid():
     dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN)
     dbi.update(_get_text('event_rrule_recuid_invalid_tzid'), href='12345.ics', etag='abcd',
                calendar=calname)
-    events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)),
-                               BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)))
-    events = sorted(events)
+    events = dbi.get_localized(
+        BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)),
+        BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)))
+    events = sorted(events, key=itemgetter(2))
     assert len(events) == 6
 
-    assert events[0].start == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
-    assert events[1].start == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
-    assert events[2].start == BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0))
-    assert events[3].start == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
-    assert events[4].start == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
-    assert events[5].start == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
+    assert events[0][2] == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
+    assert events[1][2] == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
+    assert events[2][2] == BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0))
+    assert events[3][2] == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
+    assert events[4][2] == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
+    assert events[5][2] == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
 
 
 event_rrule_recurrence_id_reverse = """
@@ -147,22 +97,24 @@ def test_event_rrule_recurrence_id_reverse():
     """
     dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN)
     assert dbi.list(calname) == list()
-    events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
-                               BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)))
+    events = dbi.get_localized(
+        BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
+        BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)))
     assert list(events) == list()
     dbi.update(event_rrule_recurrence_id_reverse, href='12345.ics', etag='abcd', calendar=calname)
     assert dbi.list(calname) == [('12345.ics', 'abcd')]
-    events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
-                               BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)))
-    events = sorted(events, key=lambda x: x.start)
+    events = dbi.get_localized(
+        BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
+        BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)))
+    events = sorted(events, key=itemgetter(2))
     assert len(events) == 6
 
-    assert events[0].start == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
-    assert events[1].start == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
-    assert events[2].start == BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0))
-    assert events[3].start == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
-    assert events[4].start == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
-    assert events[5].start == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
+    assert events[0][2] == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
+    assert events[1][2] == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
+    assert events[2][2] == BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0))
+    assert events[3][2] == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
+    assert events[4][2] == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
+    assert events[5][2] == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
 
 
 def test_event_rrule_recurrence_id_update_with_exclude():
@@ -176,13 +128,13 @@ def test_event_rrule_recurrence_id_update_with_exclude():
                href='12345.ics', etag='abcd', calendar=calname)
     events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)),
                                BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)))
-    events = sorted(events, key=lambda x: x.start)
+    events = sorted(events, key=itemgetter(2))
     assert len(events) == 5
-    assert events[0].start == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
-    assert events[1].start == BERLIN.localize(dt.datetime(2014, 7, 7, 7, 0))
-    assert events[2].start == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
-    assert events[3].start == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
-    assert events[4].start == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
+    assert events[0][2] == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
+    assert events[1][2] == BERLIN.localize(dt.datetime(2014, 7, 7, 7, 0))
+    assert events[2][2] == BERLIN.localize(dt.datetime(2014, 7, 21, 7, 0))
+    assert events[3][2] == BERLIN.localize(dt.datetime(2014, 7, 28, 7, 0))
+    assert events[4][2] == BERLIN.localize(dt.datetime(2014, 8, 4, 7, 0))
 
 
 def test_event_recuid_no_master():
@@ -196,13 +148,11 @@ def test_event_recuid_no_master():
     events = dbi.get_floating(
         dt.datetime(2017, 3, 1, 0, 0), dt.datetime(2017, 4, 1, 0, 0),
     )
-    events = sorted(events, key=lambda x: x.start)
+    events = sorted(events, key=itemgetter(2))
     assert len(events) == 1
-    assert events[0].start == dt.datetime(2017, 3, 29, 16)
-    assert events[0].end == dt.datetime(2017, 3, 29, 16, 25)
-    assert events[0].format(
-        '{title}', relative_to=dt.date(2017, 3, 29)
-    ) == 'Infrastructure Planning\x1b[0m'
+    assert events[0][2] == dt.datetime(2017, 3, 29, 16)
+    assert events[0][3] == dt.datetime(2017, 3, 29, 16, 25)
+    assert 'SUMMARY:Infrastructure Planning' in events[0][0]
 
 
 def test_event_recuid_rrule_no_master():
@@ -218,14 +168,13 @@ def test_event_recuid_rrule_no_master():
     events = dbi.get_floating(
         dt.datetime(2010, 1, 1, 0, 0), dt.datetime(2020, 1, 1, 0, 0),
     )
-    events = sorted(events, key=lambda x: x.start)
+    events = sorted(events, key=itemgetter(2))
     assert len(list(events)) == 2
-    assert events[0].start == dt.datetime(2014, 6, 30, 7, 30)
-    assert events[0].end == dt.datetime(2014, 6, 30, 12, 0)
-    assert events[1].start == dt.datetime(2014, 7, 7, 8, 30)
-    assert events[1].end == dt.datetime(2014, 7, 7, 12, 0)
+    assert events[0][2] == dt.datetime(2014, 6, 30, 7, 30)
+    assert events[0][3] == dt.datetime(2014, 6, 30, 12, 0)
+    assert events[1][2] == dt.datetime(2014, 7, 7, 8, 30)
+    assert events[1][3] == dt.datetime(2014, 7, 7, 12, 0)
     events = dbi.search('VEVENT')
-    events = sorted(events, key=lambda x: x.start)
     assert len(list(events)) == 2
 
 
@@ -238,10 +187,8 @@ def test_no_valid_timezone():
     events = sorted(list(events))
     assert len(events) == 1
     event = events[0]
-    assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30))
-    assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30))
-    assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30))
-    assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30))
+    assert event[2] == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30))
+    assert event[3] == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30))
 
 
 def test_event_delete():
@@ -317,27 +264,26 @@ def test_event_rrule_this_and_future():
     assert dbi.list(calname) == [('12345.ics', 'abcd')]
     events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)),
                                BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)))
-    events = sorted(events, key=lambda x: x.start)
+    events = sorted(events, key=itemgetter(2))
     assert len(events) == 6
 
-    assert events[0].start == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
-    assert events[1].start == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
-    assert events[2].start == BERLIN.localize(dt.datetime(2014, 7, 14, 9, 0))
-    assert events[3].start == BERLIN.localize(dt.datetime(2014, 7, 21, 9, 0))
-    assert events[4].start == BERLIN.localize(dt.datetime(2014, 7, 28, 9, 0))
-    assert events[5].start == BERLIN.localize(dt.datetime(2014, 8, 4, 9, 0))
-
-    assert events[0].end == BERLIN.localize(dt.datetime(2014, 6, 30, 12, 0))
-    assert events[1].end == BERLIN.localize(dt.datetime(2014, 7, 7, 18, 0))
-    assert events[2].end == BERLIN.localize(dt.datetime(2014, 7, 14, 18, 0))
-    assert events[3].end == BERLIN.localize(dt.datetime(2014, 7, 21, 18, 0))
-    assert events[4].end == BERLIN.localize(dt.datetime(2014, 7, 28, 18, 0))
-    assert events[5].end == BERLIN.localize(dt.datetime(2014, 8, 4, 18, 0))
-
-    assert str(events[0].summary) == 'Arbeit'
+    assert events[0][2] == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
+    assert events[1][2] == BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0))
+    assert events[2][2] == BERLIN.localize(dt.datetime(2014, 7, 14, 9, 0))
+    assert events[3][2] == BERLIN.localize(dt.datetime(2014, 7, 21, 9, 0))
+    assert events[4][2] == BERLIN.localize(dt.datetime(2014, 7, 28, 9, 0))
+    assert events[5][2] == BERLIN.localize(dt.datetime(2014, 8, 4, 9, 0))
+
+    assert events[0][3] == BERLIN.localize(dt.datetime(2014, 6, 30, 12, 0))
+    assert events[1][3] == BERLIN.localize(dt.datetime(2014, 7, 7, 18, 0))
+    assert events[2][3] == BERLIN.localize(dt.datetime(2014, 7, 14, 18, 0))
+    assert events[3][3] == BERLIN.localize(dt.datetime(2014, 7, 21, 18, 0))
+    assert events[4][3] == BERLIN.localize(dt.datetime(2014, 7, 28, 18, 0))
+    assert events[5][3] == BERLIN.localize(dt.datetime(2014, 8, 4, 18, 0))
+
+    assert 'SUMMARY:Arbeit\n' in events[0][0]
     for num, event in enumerate(events[1:]):
-        assert event.raw  # just making sure we don't raise any exception
-        assert str(event.summary) == 'Arbeit (lang)'
+        assert 'SUMMARY:Arbeit (lang)\n' in event[0]
 
 
 event_rrule_this_and_future_multi_day_shift = \
@@ -351,26 +297,26 @@ def test_event_rrule_this_and_future_multi_day_shift():
     assert dbi.list(calname) == [('12345.ics', 'abcd')]
     events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)),
                                BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)))
-    events = sorted(events, key=lambda x: x.start)
+    events = sorted(events, key=itemgetter(2))
     assert len(events) == 6
 
-    assert events[0].start == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
-    assert events[1].start == BERLIN.localize(dt.datetime(2014, 7, 8, 9, 0))
-    assert events[2].start == BERLIN.localize(dt.datetime(2014, 7, 15, 9, 0))
-    assert events[3].start == BERLIN.localize(dt.datetime(2014, 7, 22, 9, 0))
-    assert events[4].start == BERLIN.localize(dt.datetime(2014, 7, 29, 9, 0))
-    assert events[5].start == BERLIN.localize(dt.datetime(2014, 8, 5, 9, 0))
-
-    assert events[0].end == BERLIN.localize(dt.datetime(2014, 6, 30, 12, 0))
-    assert events[1].end == BERLIN.localize(dt.datetime(2014, 7, 9, 15, 0))
-    assert events[2].end == BERLIN.localize(dt.datetime(2014, 7, 16, 15, 0))
-    assert events[3].end == BERLIN.localize(dt.datetime(2014, 7, 23, 15, 0))
-    assert events[4].end == BERLIN.localize(dt.datetime(2014, 7, 30, 15, 0))
-    assert events[5].end == BERLIN.localize(dt.datetime(2014, 8, 6, 15, 0))
-
-    assert str(events[0].summary) == 'Arbeit'
+    assert events[0][2] == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0))
+    assert events[1][2] == BERLIN.localize(dt.datetime(2014, 7, 8, 9, 0))
+    assert events[2][2] == BERLIN.localize(dt.datetime(2014, 7, 15, 9, 0))
+    assert events[3][2] == BERLIN.localize(dt.datetime(2014, 7, 22, 9, 0))
+    assert events[4][2] == BERLIN.localize(dt.datetime(2014, 7, 29, 9, 0))
+    assert events[5][2] == BERLIN.localize(dt.datetime(2014, 8, 5, 9, 0))
+
+    assert events[0][3] == BERLIN.localize(dt.datetime(2014, 6, 30, 12, 0))
+    assert events[1][3] == BERLIN.localize(dt.datetime(2014, 7, 9, 15, 0))
+    assert events[2][3] == BERLIN.localize(dt.datetime(2014, 7, 16, 15, 0))
+    assert events[3][3] == BERLIN.localize(dt.datetime(2014, 7, 23, 15, 0))
+    assert events[4][3] == BERLIN.localize(dt.datetime(2014, 7, 30, 15, 0))
+    assert events[5][3] == BERLIN.localize(dt.datetime(2014, 8, 6, 15, 0))
+
+    assert 'SUMMARY:Arbeit\n' in events[0][0]
     for event in events[1:]:
-        assert str(event.summary) == 'Arbeit (lang)'
+        assert 'SUMMARY:Arbeit (lang)\n' in event[0]
 
 
 event_rrule_this_and_future_allday_temp = """
@@ -404,32 +350,23 @@ def test_event_rrule_this_and_future_allday():
     events = list(dbi.get_floating(dt.datetime(2014, 4, 30, 0, 0), dt.datetime(2014, 9, 27, 0, 0)))
     assert len(events) == 6
 
-    assert events[0].start == dt.date(2014, 6, 30)
-    assert events[1].start == dt.date(2014, 7, 8)
-    assert events[2].start == dt.date(2014, 7, 15)
-    assert events[3].start == dt.date(2014, 7, 22)
-    assert events[4].start == dt.date(2014, 7, 29)
-    assert events[5].start == dt.date(2014, 8, 5)
-
-    assert events[0].end == dt.date(2014, 6, 30)
-    assert events[1].end == dt.date(2014, 7, 8)
-    assert events[2].end == dt.date(2014, 7, 15)
-    assert events[3].end == dt.date(2014, 7, 22)
-    assert events[4].end == dt.date(2014, 7, 29)
-    assert events[5].end == dt.date(2014, 8, 5)
-
-    assert str(events[0].summary) == 'Arbeit'
+    assert events[0][2] == dt.date(2014, 6, 30)
+    assert events[1][2] == dt.date(2014, 7, 8)
+    assert events[2][2] == dt.date(2014, 7, 15)
+    assert events[3][2] == dt.date(2014, 7, 22)
+    assert events[4][2] == dt.date(2014, 7, 29)
+    assert events[5][2] == dt.date(2014, 8, 5)
+
+    assert events[0][3] == dt.date(2014, 7, 1)
+    assert events[1][3] == dt.date(2014, 7, 9)
+    assert events[2][3] == dt.date(2014, 7, 16)
+    assert events[3][3] == dt.date(2014, 7, 23)
+    assert events[4][3] == dt.date(2014, 7, 30)
+    assert events[5][3] == dt.date(2014, 8, 6)
+
+    assert 'SUMMARY:Arbeit\n' in events[0][0]
     for event in events[1:]:
-        assert str(event.summary) == 'Arbeit (lang)'
-
-    events = list(dbi.get_floating(
-        dt.datetime(2014, 4, 30, 0, 0),
-        dt.datetime(2014, 9, 27, 0, 0),
-        minimal=True,
-    ))
-    assert len(events) == 6
-    for event in events:
-        assert isinstance(event, EventStandIn)
+        assert 'SUMMARY:Arbeit (lang)\n' in event[0]
 
 
 def test_event_rrule_this_and_future_allday_prior():
@@ -443,23 +380,23 @@ def test_event_rrule_this_and_future_allday_prior():
 
     assert len(events) == 6
 
-    assert events[0].start == dt.date(2014, 6, 30)
-    assert events[1].start == dt.date(2014, 7, 5)
-    assert events[2].start == dt.date(2014, 7, 12)
-    assert events[3].start == dt.date(2014, 7, 19)
-    assert events[4].start == dt.date(2014, 7, 26)
-    assert events[5].start == dt.date(2014, 8, 2)
-
-    assert events[0].end == dt.date(2014, 6, 30)
-    assert events[1].end == dt.date(2014, 7, 5)
-    assert events[2].end == dt.date(2014, 7, 12)
-    assert events[3].end == dt.date(2014, 7, 19)
-    assert events[4].end == dt.date(2014, 7, 26)
-    assert events[5].end == dt.date(2014, 8, 2)
-
-    assert str(events[0].summary) == 'Arbeit'
+    assert events[0][2] == dt.date(2014, 6, 30)
+    assert events[1][2] == dt.date(2014, 7, 5)
+    assert events[2][2] == dt.date(2014, 7, 12)
+    assert events[3][2] == dt.date(2014, 7, 19)
+    assert events[4][2] == dt.date(2014, 7, 26)
+    assert events[5][2] == dt.date(2014, 8, 2)
+
+    assert events[0][3] == dt.date(2014, 7, 1)
+    assert events[1][3] == dt.date(2014, 7, 6)
+    assert events[2][3] == dt.date(2014, 7, 13)
+    assert events[3][3] == dt.date(2014, 7, 20)
+    assert events[4][3] == dt.date(2014, 7, 27)
+    assert events[5][3] == dt.date(2014, 8, 3)
+
+    assert 'SUMMARY:Arbeit\n' in events[0][0]
     for event in events[1:]:
-        assert str(event.summary) == 'Arbeit (lang)'
+        assert 'SUMMARY:Arbeit (lang)\n' in event[0]
 
 
 event_rrule_multi_this_and_future_allday = """BEGIN:VCALENDAR
@@ -497,25 +434,25 @@ def test_event_rrule_multi_this_and_future_allday():
     )
     assert len(events) == 6
 
-    assert events[0].start == dt.date(2014, 6, 30)
-    assert events[1].start == dt.date(2014, 7, 12)
-    assert events[2].start == dt.date(2014, 7, 17)
-    assert events[3].start == dt.date(2014, 7, 19)
-    assert events[4].start == dt.date(2014, 7, 24)
-    assert events[5].start == dt.date(2014, 7, 31)
-
-    assert events[0].end == dt.date(2014, 6, 30)
-    assert events[1].end == dt.date(2014, 7, 13)
-    assert events[2].end == dt.date(2014, 7, 17)
-    assert events[3].end == dt.date(2014, 7, 20)
-    assert events[4].end == dt.date(2014, 7, 24)
-    assert events[5].end == dt.date(2014, 7, 31)
-
-    assert str(events[0].summary) == 'Arbeit'
+    assert events[0][2] == dt.date(2014, 6, 30)
+    assert events[1][2] == dt.date(2014, 7, 12)
+    assert events[2][2] == dt.date(2014, 7, 17)
+    assert events[3][2] == dt.date(2014, 7, 19)
+    assert events[4][2] == dt.date(2014, 7, 24)
+    assert events[5][2] == dt.date(2014, 7, 31)
+
+    assert events[0][3] == dt.date(2014, 7, 1)
+    assert events[1][3] == dt.date(2014, 7, 14)
+    assert events[2][3] == dt.date(2014, 7, 18)
+    assert events[3][3] == dt.date(2014, 7, 21)
+    assert events[4][3] == dt.date(2014, 7, 25)
+    assert events[5][3] == dt.date(2014, 8, 1)
+
+    assert 'SUMMARY:Arbeit\n' in events[0][0]
     for event in [events[1], events[3]]:
-        assert str(event.summary) == 'Arbeit (lang)'
+        assert 'SUMMARY:Arbeit (lang)\n' in event[0]
     for event in [events[2], events[4], events[5]]:
-        assert str(event.summary) == 'Arbeit (neu)'
+        assert 'SUMMARY:Arbeit (neu)\n' in event[0]
 
 
 master = """BEGIN:VEVENT
@@ -591,8 +528,9 @@ def test_two_calendars_same_uid():
     events_c = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
                                       BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0))))
     assert len(events_c) == 8
-    assert [event.calendar for event in events_c].count(home) == 4
-    assert [event.calendar for event in events_c].count(work) == 4
+    # count events from a given calendar
+    assert [event[6] for event in events_c].count(home) == 4
+    assert [event[6] for event in events_c].count(work) == 4
 
     dbi.delete('12345.ics', calendar=home)
     dbi.calendars = [home]
@@ -606,8 +544,8 @@ def test_two_calendars_same_uid():
     dbi.calendars = [work, home]
     events_c = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)),
                                       BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0))))
-    assert [event.calendar for event in events_c].count('home') == 0
-    assert [event.calendar for event in events_c].count('work') == 4
+    assert [event[6] for event in events_c].count('home') == 0
+    assert [event[6] for event in events_c].count('work') == 4
     assert dbi.list(home) == []
     assert dbi.list(work) == [('12345.ics', 'abcd')]
 
@@ -623,19 +561,6 @@ def test_update_one_should_not_affect_others():
     assert len(events) == 1
 
 
-def test_zulu_events():
-    """test if events in Zulu time are correctly recognized as locaized events"""
-    db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN)
-    db.update(_get_text('event_dt_simple_zulu'), href='event_zulu', calendar=calname)
-    events = db.get_localized(BERLIN.localize(dt.datetime(2014, 4, 9, 0, 0)),
-                              BERLIN.localize(dt.datetime(2014, 4, 10, 0, 0)))
-    events = list(events)
-    assert len(events) == 1
-    event = events[0]
-    assert type(event) == LocalizedEvent
-    assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 11, 30))
-
-
 def test_no_dtend():
     """test support for events with no dtend"""
     db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN)
@@ -643,8 +568,8 @@ def test_no_dtend():
     events = db.get_floating(
         dt.datetime(2016, 1, 16, 0, 0), dt.datetime(2016, 1, 17, 0, 0))
     event = list(events)[0]
-    assert event.start == dt.date(2016, 1, 16)
-    assert event.end == dt.date(2016, 1, 16)
+    assert event[2] == dt.date(2016, 1, 16)
+    assert event[3] == dt.date(2016, 1, 17)
 
 
 event_rdate_period = """BEGIN:VEVENT
@@ -741,17 +666,13 @@ def test_birthdays():
     db.update_birthday(card, 'unix.vcf', calendar=calname)
     events = list(db.get_floating(start, end))
     assert len(events) == 1
-    assert events[0].summary == 'Unix\'s 0th birthday'
-    events = list(db.get_floating(dt.datetime(2016, 3, 11, 0, 0),
-                                  dt.datetime(2016, 3, 11, 23, 59, 59, 999)))
-    assert events[0].summary == 'Unix\'s 45th birthday'
+    assert 'SUMMARY:Unix\'s birthday' in events[0][0]
 
-    assert list(
-        db.get_floating(dt.datetime(2013, 3, 11), dt.datetime(2013, 3, 11))
-    )[0].summary == 'Unix\'s 42nd birthday'
-    assert list(
-        db.get_floating(dt.datetime(2014, 3, 11), dt.datetime(2014, 3, 11))
-    )[0].summary == 'Unix\'s 43rd birthday'
+    events = list(
+        db.get_floating(
+            dt.datetime(2016, 3, 11, 0, 0),
+            dt.datetime(2016, 3, 11, 23, 59, 59, 999)))
+    assert 'SUMMARY:Unix\'s birthday' in events[0][0]
 
 
 def test_birthdays_update():
@@ -761,45 +682,6 @@ def test_birthdays_update():
     db.update_birthday(card, 'unix.vcf', calendar=calname)
 
 
-def test_birthdays_29feb():
-    """test how we deal with birthdays on 29th of feb in leap years"""
-    db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN)
-    db.update_birthday(card_29thfeb, 'leap.vcf', calendar=calname)
-    events = list(
-        db.get_floating(dt.datetime(2004, 1, 1, 0, 0), dt.datetime(2004, 12, 31))
-    )
-    assert len(events) == 1
-    assert events[0].summary == 'leapyear\'s 4th birthday (29th of Feb.)'
-    assert events[0].start == dt.date(2004, 2, 29)
-    events = list(
-        db.get_floating(dt.datetime(2005, 1, 1, 0, 0), dt.datetime(2005, 12, 31))
-    )
-    assert len(events) == 1
-    assert events[0].summary == 'leapyear\'s 5th birthday (29th of Feb.)'
-    assert events[0].start == dt.date(2005, 3, 1)
-    assert list(
-        db.get_floating(dt.datetime(2001, 1, 1), dt.datetime(2001, 12, 31))
-    )[0].summary == 'leapyear\'s 1st birthday (29th of Feb.)'
-    assert list(
-        db.get_floating(dt.datetime(2002, 1, 1), dt.datetime(2002, 12, 31))
-    )[0].summary == 'leapyear\'s 2nd birthday (29th of Feb.)'
-    assert list(
-        db.get_floating(dt.datetime(2003, 1, 1), dt.datetime(2003, 12, 31))
-    )[0].summary == 'leapyear\'s 3rd birthday (29th of Feb.)'
-    assert list(
-        db.get_floating(dt.datetime(2023, 1, 1), dt.datetime(2023, 12, 31))
-    )[0].summary == 'leapyear\'s 23rd birthday (29th of Feb.)'
-
-
-def test_birthdays_no_year():
-    db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN)
-    assert list(db.get_floating(start, end)) == list()
-    db.update_birthday(card_no_year, 'unix.vcf', calendar=calname)
-    events = list(db.get_floating(start, end))
-    assert len(events) == 1
-    assert events[0].summary == 'Unix\'s birthday'
-
-
 def test_birthdays_no_fn():
     db = backend.SQLiteDb(['home'], ':memory:', locale=LOCALE_BERLIN)
     assert list(db.get_floating(dt.datetime(1941, 9, 9, 0, 0),
@@ -808,7 +690,7 @@ def test_birthdays_no_fn():
     events = list(db.get_floating(dt.datetime(1941, 9, 9, 0, 0),
                                   dt.datetime(1941, 9, 9, 23, 59, 59, 9999)))
     assert len(events) == 1
-    assert events[0].summary == 'Dennis MacAlistair Ritchie\'s 0th birthday'
+    assert 'SUMMARY:Dennis MacAlistair Ritchie\'s birthday' in events[0][0]
 
 
 def test_birthday_does_not_parse():
diff --git a/tests/conftest.py b/tests/conftest.py
index 7175574..90fb7b9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -24,6 +24,21 @@ def coll_vdirs(tmpdir):
     return coll, vdirs
 
 
+@pytest.fixture
+def coll_vdirs_birthday(tmpdir):
+    calendars, vdirs = dict(), dict()
+    for name in example_cals:
+        path = str(tmpdir) + '/' + name
+        os.makedirs(path, mode=0o770)
+        readonly = True if name == 'a_calendar' else False
+        calendars[name] = {'name': name, 'path': path, 'color': 'dark blue',
+                           'readonly': readonly, 'unicode_symbols': True, 'ctype': 'birthdays'}
+        vdirs[name] = Vdir(path, '.vcf')
+    coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN)
+    coll.default_calendar_name = cal1
+    return coll, vdirs
+
+
 @pytest.fixture(autouse=True)
 def never_echo_bytes(monkeypatch):
     '''Click's echo function will not strip colorcodes if we call `click.echo`
diff --git a/tests/event_test.py b/tests/event_test.py
index 4e0a73f..d385272 100644
--- a/tests/event_test.py
+++ b/tests/event_test.py
@@ -334,6 +334,13 @@ def test_event_raw_UTC():
         '''END:VCALENDAR\r\n'''])
 
 
+def test_zulu_events():
+    """test if events in Zulu time are correctly recognized as localized events"""
+    event = Event.fromString(_get_text('event_dt_simple_zulu'), **EVENT_KWARGS)
+    assert type(event) == LocalizedEvent
+    assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 11, 30))
+
+
 def test_dtend_equals_dtstart():
     event = Event.fromString(_get_text('event_d_same_start_end'),
                              calendar='foobar', locale=LOCALE_BERLIN)
diff --git a/tests/khalendar_test.py b/tests/khalendar_test.py
index 2aa7b5d..47aae9a 100644
--- a/tests/khalendar_test.py
+++ b/tests/khalendar_test.py
@@ -13,12 +13,16 @@ from khal.khalendar.event import Event
 from khal.khalendar.vdir import Item
 
 from . import utils
-from .utils import _get_text, cal1, cal2, cal3, normalize_component
+from .utils import (_get_text, cal1, cal2, cal3, normalize_component, DumbItem,
+                    BERLIN, LONDON, SYDNEY, LOCALE_SYDNEY, LOCALE_BERLIN)
 
 today = dt.date.today()
 yesterday = today - dt.timedelta(days=1)
 tomorrow = today + dt.timedelta(days=1)
 
+aday = dt.date(2014, 4, 9)
+bday = dt.date(2014, 4, 10)
+
 event_allday_template = """BEGIN:VEVENT
 SEQUENCE:0
 UID:uid3@host1.com
@@ -92,30 +96,21 @@ class TestCalendar(object):
 class TestVdirsyncerCompat(object):
     def test_list(self, coll_vdirs):
         coll, vdirs = coll_vdirs
-        event = Event.fromString(event_d, calendar=cal1, locale=utils.LOCALE_BERLIN)
+        event = Event.fromString(_get_text('event_d'), calendar=cal1, locale=LOCALE_BERLIN)
         assert event.etag is None
         assert event.href is None
         coll.new(event)
         assert event.etag is not None
         assert event.href == 'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU.ics'
-        event = Event.fromString(event_today, calendar=cal1, locale=utils.LOCALE_BERLIN)
+        event = Event.fromString(event_today, calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event)
-        hrefs = sorted(href for href, uid in coll._backend.list(cal1))
-        assert set(str(coll._backend.get(href, calendar=cal1).uid) for href in hrefs) == set((
+        hrefs = sorted(href for href, etag in coll._backend.list(cal1))
+        assert set(str(coll.get_event(href, calendar=cal1).uid) for href in hrefs) == set((
             'uid3@host1.com',
             'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU',
         ))
 
 
-aday = dt.date(2014, 4, 9)
-bday = dt.date(2014, 4, 10)
-
-
-event_dt = _get_text('event_dt_simple')
-event_d = _get_text('event_d')
-event_d_no_value = _get_text('event_d_no_value')
-
-
 class TestCollection(object):
 
     astart = dt.datetime.combine(aday, dt.time.min)
@@ -134,7 +129,7 @@ class TestCollection(object):
             'work': {'name': 'work', 'path': str(tmpdir), 'readonly': True},
         }
         coll = CalendarCollection(
-            calendars=calendars, locale=utils.LOCALE_BERLIN, dbpath=':memory:',
+            calendars=calendars, locale=LOCALE_BERLIN, dbpath=':memory:',
         )
         assert coll.default_calendar_name is None
         with pytest.raises(ValueError):
@@ -158,8 +153,9 @@ class TestCollection(object):
     def test_insert(self, coll_vdirs):
         """insert a localized event"""
         coll, vdirs = coll_vdirs
-        event = Event.fromString(event_dt, calendar=cal1, locale=utils.LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.new(
+            Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN),
+            cal1)
         events = list(coll.get_localized(self.astart_berlin, self.aend_berlin))
         assert len(events) == 1
         assert events[0].color == 'dark blue'
@@ -178,7 +174,7 @@ class TestCollection(object):
     def test_insert_d(self, coll_vdirs):
         """insert a floating event"""
         coll, vdirs = coll_vdirs
-        event = Event.fromString(event_d, calendar=cal1, locale=utils.LOCALE_BERLIN)
+        event = Event.fromString(_get_text('event_d'), calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event, cal1)
         events = list(coll.get_events_on(aday))
         assert len(events) == 1
@@ -192,9 +188,10 @@ class TestCollection(object):
     def test_insert_d_no_value(self, coll_vdirs):
         """insert a date event with no VALUE=DATE option"""
         coll, vdirs = coll_vdirs
-
-        event = Event.fromString(event_d_no_value, calendar=cal1, locale=utils.LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.new(
+            Event.fromString(
+                _get_text('event_d_no_value'), calendar=cal1, locale=LOCALE_BERLIN),
+            cal1)
         events = list(coll.get_events_on(aday))
         assert len(events) == 1
         assert events[0].calendar == cal1
@@ -207,7 +204,7 @@ class TestCollection(object):
         """test getting an event by its href"""
         coll, vdirs = coll_vdirs
         event = Event.fromString(
-            event_dt, href='xyz.ics', calendar=cal1, locale=utils.LOCALE_BERLIN,
+            _get_text('event_dt_simple'), href='xyz.ics', calendar=cal1, locale=LOCALE_BERLIN,
         )
         coll.new(event, cal1)
         event_from_db = coll.get_event(SIMPLE_EVENT_UID + '.ics', cal1)
@@ -218,7 +215,7 @@ class TestCollection(object):
     def test_change(self, coll_vdirs):
         """moving an event from one calendar to another"""
         coll, vdirs = coll_vdirs
-        event = Event.fromString(event_dt, calendar=cal1, locale=utils.LOCALE_BERLIN)
+        event = Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event, cal1)
         event = list(coll.get_events_on(aday))[0]
         assert event.calendar == cal1
@@ -232,7 +229,7 @@ class TestCollection(object):
         """updating one event"""
         coll, vdirs = coll_vdirs
         event = Event.fromString(
-            _get_text('event_dt_simple'), calendar=cal1, locale=utils.LOCALE_BERLIN)
+            _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event, cal1)
         events = coll.get_events_on(aday)
         event = list(events)[0]
@@ -251,7 +248,7 @@ class TestCollection(object):
         anend = bday + dt.timedelta(hours=1)
         event = khal.utils.new_event(
             dtstart=bday, dtend=anend, summary="hi", timezone=utils.BERLIN,
-            locale=utils.LOCALE_BERLIN,
+            locale=LOCALE_BERLIN,
         )
         event = coll.new_event(event.to_ical(), coll.default_calendar_name)
         assert event.allday is False
@@ -260,7 +257,7 @@ class TestCollection(object):
         coll, vdirs = coll_vdirs
         coll._calendars[cal1]['readonly'] = True
         coll._calendars[cal3]['readonly'] = True
-        event = Event.fromString(event_dt, calendar=cal1, locale=utils.LOCALE_BERLIN)
+        event = Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
 
         with pytest.raises(khal.khalendar.exceptions.ReadOnlyCalendarError):
             coll.new(event, cal1)
@@ -272,11 +269,11 @@ class TestCollection(object):
         coll, vdirs = coll_vdirs
         assert len(list(coll.search('Event'))) == 0
         event = Event.fromString(
-            _get_text('event_dt_simple'), calendar=cal1, locale=utils.LOCALE_BERLIN)
+            _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event, cal1)
         assert len(list(coll.search('Event'))) == 1
         event = Event.fromString(
-            _get_text('event_dt_floating'), calendar=cal1, locale=utils.LOCALE_BERLIN)
+            _get_text('event_dt_floating'), calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event, cal1)
         assert len(list(coll.search('Search for me'))) == 1
         assert len(list(coll.search('Event'))) == 2
@@ -287,7 +284,7 @@ class TestCollection(object):
         coll, vdirs = coll_vdirs
         assert len(list(coll.search('Event'))) == 0
         event = Event.fromString(
-            _get_text('event_dt_recuid_no_master'), calendar=cal1, locale=utils.LOCALE_BERLIN)
+            _get_text('event_dt_recuid_no_master'), calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event, cal1)
         assert len(list(coll.search('Event'))) == 1
 
@@ -297,7 +294,7 @@ class TestCollection(object):
         coll, vdirs = coll_vdirs
         assert len(list(coll.search('Event'))) == 0
         event = Event.fromString(
-            _get_text('event_dt_multi_recuid_no_master'), calendar=cal1, locale=utils.LOCALE_BERLIN)
+            _get_text('event_dt_multi_recuid_no_master'), calendar=cal1, locale=LOCALE_BERLIN)
         coll.new(event, cal1)
         events = list(sorted(coll.search('Event')))
         assert len(events) == 2
@@ -311,9 +308,9 @@ class TestCollection(object):
             calendars with the same filename"""
             coll, vdirs = coll_vdirs
             event1 = Event.fromString(
-                _get_text('event_dt_simple'), calendar=cal1, locale=utils.LOCALE_BERLIN)
+                _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
             event2 = Event.fromString(
-                _get_text('event_dt_simple'), calendar=cal2, locale=utils.LOCALE_BERLIN)
+                _get_text('event_dt_simple'), calendar=cal2, locale=LOCALE_BERLIN)
             coll.new(event1, cal1)
             sleep(sleep_time)  # make sure the etags are different
             coll.new(event2, cal2)
@@ -339,7 +336,7 @@ class TestDbCreation(object):
 
         assert not os.path.isdir(dbdir)
         calendars = {cal1: {'name': cal1, 'path': vdirpath}}
-        CalendarCollection(calendars, dbpath=dbpath, locale=utils.LOCALE_BERLIN)
+        CalendarCollection(calendars, dbpath=dbpath, locale=LOCALE_BERLIN)
         assert os.path.isdir(dbdir)
 
     def test_failed_create_db(self, tmpdir):
@@ -349,7 +346,56 @@ class TestDbCreation(object):
 
         calendars = {cal1: {'name': cal1, 'path': str(tmpdir)}}
         with pytest.raises(CouldNotCreateDbDir):
-            CalendarCollection(calendars, dbpath=dbpath, locale=utils.LOCALE_BERLIN)
+            CalendarCollection(calendars, dbpath=dbpath, locale=LOCALE_BERLIN)
+
+
+def test_event_different_timezones(coll_vdirs):
+    coll, vdirs = coll_vdirs
+    vdirs[cal1].upload(DumbItem(_get_text('event_dt_london'), uid='12345'))
+    coll.update_db()
+
+    events = coll.get_localized(
+        BERLIN.localize(dt.datetime(2014, 4, 9, 0, 0)),
+        BERLIN.localize(dt.datetime(2014, 4, 9, 23, 59)),
+    )
+    events = list(events)
+    assert len(events) == 1
+    event = events[0]
+    assert event.start_local == LONDON.localize(dt.datetime(2014, 4, 9, 14))
+    assert event.end_local == LONDON.localize(dt.datetime(2014, 4, 9, 19))
+    assert event.start == LONDON.localize(dt.datetime(2014, 4, 9, 14))
+    assert event.end == LONDON.localize(dt.datetime(2014, 4, 9, 19))
+
+    # no event scheduled on the next day
+    events = coll.get_localized(
+        BERLIN.localize(dt.datetime(2014, 4, 10, 0, 0)),
+        BERLIN.localize(dt.datetime(2014, 4, 10, 23, 59)),
+    )
+    events = list(events)
+    assert len(events) == 0
+
+    # now setting the local_timezone to Sydney
+    coll.locale = LOCALE_SYDNEY
+    events = coll.get_localized(
+        SYDNEY.localize(dt.datetime(2014, 4, 9, 0, 0)),
+        SYDNEY.localize(dt.datetime(2014, 4, 9, 23, 59)),
+    )
+    events = list(events)
+    assert len(events) == 1
+    event = events[0]
+    assert event.start_local == SYDNEY.localize(dt.datetime(2014, 4, 9, 23))
+    assert event.end_local == SYDNEY.localize(dt.datetime(2014, 4, 10, 4))
+    assert event.start == LONDON.localize(dt.datetime(2014, 4, 9, 14))
+    assert event.end == LONDON.localize(dt.datetime(2014, 4, 9, 19))
+
+    # the event spans midnight Sydney, therefor it should also show up on the
+    # next day
+    events = coll.get_localized(SYDNEY.localize(dt.datetime(2014, 4, 10, 0, 0)),
+                                SYDNEY.localize(dt.datetime(2014, 4, 10, 23, 59)))
+    events = list(events)
+    assert len(events) == 1
+    assert event.start_local == SYDNEY.localize(dt.datetime(2014, 4, 9, 23))
+    assert event.end_local == SYDNEY.localize(dt.datetime(2014, 4, 10, 4))
 
 
 def test_default_calendar(coll_vdirs, sleep_time):
@@ -426,3 +472,84 @@ def test_only_update_old_event(coll_vdirs, monkeypatch, sleep_time):
     coll.update_db()
     sleep(sleep_time)
     assert updated_hrefs == [href_three]
+
+
+card = """BEGIN:VCARD
+VERSION:3.0
+FN:Unix
+BDAY:19710311
+END:VCARD
+"""
+
+card_29thfeb = """BEGIN:VCARD
+VERSION:3.0
+FN:leapyear
+BDAY:20000229
+END:VCARD
+"""
+
+card_no_year = """BEGIN:VCARD
+VERSION:3.0
+FN:Unix
+BDAY:--0311
+END:VCARD
+"""
+
+
+def test_birthdays(coll_vdirs_birthday):
+    coll, vdirs = coll_vdirs_birthday
+    assert list(
+        coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59))
+    ) == list()
+    vdirs[cal1].upload(DumbItem(card, 'unix'))
+    coll.update_db()
+    assert 'Unix\'s 41st birthday' == list(
+        coll.get_floating(dt.datetime(2012, 3, 11), dt.datetime(2012, 3, 11)))[0].summary
+    assert 'Unix\'s 42nd birthday' == list(
+        coll.get_floating(dt.datetime(2013, 3, 11), dt.datetime(2013, 3, 11)))[0].summary
+    assert 'Unix\'s 43rd birthday' == list(
+        coll.get_floating(dt.datetime(2014, 3, 11), dt.datetime(2014, 3, 11)))[0].summary
+
+
+def test_birthdays_29feb(coll_vdirs_birthday):
+    """test how we deal with birthdays on 29th of feb in leap years"""
+    coll, vdirs = coll_vdirs_birthday
+    vdirs[cal1].upload(DumbItem(card_29thfeb, 'leap'))
+    coll.update_db()
+    events = list(
+        coll.get_floating(dt.datetime(2004, 1, 1, 0, 0), dt.datetime(2004, 12, 31))
+    )
+    assert len(events) == 1
+    assert events[0].summary == 'leapyear\'s 4th birthday (29th of Feb.)'
+    assert events[0].start == dt.date(2004, 2, 29)
+    events = list(
+        coll.get_floating(dt.datetime(2005, 1, 1, 0, 0), dt.datetime(2005, 12, 31))
+    )
+    assert len(events) == 1
+    assert events[0].summary == 'leapyear\'s 5th birthday (29th of Feb.)'
+    assert events[0].start == dt.date(2005, 3, 1)
+    assert list(
+        coll.get_floating(dt.datetime(2001, 1, 1), dt.datetime(2001, 12, 31))
+    )[0].summary == 'leapyear\'s 1st birthday (29th of Feb.)'
+    assert list(
+        coll.get_floating(dt.datetime(2002, 1, 1), dt.datetime(2002, 12, 31))
+    )[0].summary == 'leapyear\'s 2nd birthday (29th of Feb.)'
+    assert list(
+        coll.get_floating(dt.datetime(2003, 1, 1), dt.datetime(2003, 12, 31))
+    )[0].summary == 'leapyear\'s 3rd birthday (29th of Feb.)'
+    assert list(
+        coll.get_floating(dt.datetime(2023, 1, 1), dt.datetime(2023, 12, 31))
+    )[0].summary == 'leapyear\'s 23rd birthday (29th of Feb.)'
+    assert events[0].start == dt.date(2005, 3, 1)
+
+
+def test_birthdays_no_year(coll_vdirs_birthday):
+    coll, vdirs = coll_vdirs_birthday
+    assert list(
+        coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59))
+    ) == list()
+    vdirs[cal1].upload(DumbItem(card_no_year, 'vcard.vcf'))
+    coll.update_db()
+    events = list(coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59)))
+    assert len(events) == 1
+    assert 'Unix\'s birthday' == events[0].summary
diff --git a/tests/utils.py b/tests/utils.py
index e7f1fcd..6004339 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -122,3 +122,9 @@ def _replace_uid(event):
     event.pop('uid')
     event.add('uid', 'E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA')
     return event
+
+
+class DumbItem():
+    def __init__(self, raw, uid):
+        self.raw = raw
+        self.uid = uid