Skip to content

Commit

Permalink
Merge pull request pimutils#210 from geier/import
Browse files Browse the repository at this point in the history
added basic import functionality
  • Loading branch information
geier committed Jun 22, 2015
2 parents 38a5a25 + 28834e8 commit 514a5b1
Show file tree
Hide file tree
Showing 15 changed files with 334 additions and 91 deletions.
23 changes: 18 additions & 5 deletions khal/aux.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from datetime import time as dtime
import random
import string
import time

import icalendar
import pytz
Expand All @@ -50,8 +49,8 @@ def timefstr(dtime_list, timeformat):
"""
if len(dtime_list) == 0:
raise ValueError()
time_start = time.strptime(dtime_list[0], timeformat)
time_start = dtime(*time_start[3:5])
time_start = datetime.strptime(dtime_list[0], timeformat)
time_start = dtime(*time_start.timetuple()[3:5])
day_start = date.today()
dtstart = datetime.combine(day_start, time_start)
dtime_list.pop(0)
Expand Down Expand Up @@ -360,5 +359,19 @@ def new_event(dtstart=None, dtend=None, summary=None, timezone=None,
return event


class InvalidDate(Exception):
pass
def ics_from_list(vevent, random_uid=False):
"""convert an iterable of icalendar.Event to an icalendar.Calendar
:param random_uid: asign the same random UID to all events
:type random_uid: bool
"""
calendar = icalendar.Calendar()
calendar.add('version', '2.0')
calendar.add('prodid', '-//CALENDARSERVER.ORG//NONSGML Version 1//EN')
if random_uid:
new_uid = icalendar.vText(generate_random_uid())
for sub_event in vevent:
if random_uid:
sub_event['uid'] = new_uid
calendar.add_component(sub_event)
return calendar
29 changes: 29 additions & 0 deletions khal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,35 @@ def new(ctx, description, location, repeat, until):
until=until.split(' ') if until is not None else None,
)

@cli.command('import')
@click.option('--include-calendar', '-a', help=('The calendar to use.'),
expose_value=False, callback=_calendar_select_callback,
metavar='CAL')
@click.option('--batch', help=('do not ask for any confirmation.'),
is_flag=True)
@click.option('--random_uid', '-r', help=('Select a random uid.'),
is_flag=True)
@click.argument('ics', type=click.File('rb'))
@click.pass_context
def import_ics(ctx, ics, batch, random_uid):
'''Import events from an .ics file.
If an event with the same UID is already present in the (implicitly)
selected calendar import will ask before updating (i.e. overwriting)
that old event with the imported one, unless --batch is given, than it
will always update. If this behaviour is not desired, use the
`--random-uid` flag to generate a new, random UID.
'''
ics_str = ics.read()
controllers.import_ics(
build_collection(ctx),
ctx.obj['conf'],
ics=ics_str,
batch=batch,
random_uid=random_uid
)

@cli.command()
@calendar_selector
@click.pass_context
Expand Down
58 changes: 50 additions & 8 deletions khal/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@

from __future__ import unicode_literals

from click import echo, style
import icalendar
from click import confirm, echo, style
from vdirsyncer.utils.vobject import Item

from collections import defaultdict

import datetime
import itertools
Expand All @@ -33,9 +37,10 @@

from khal import aux, calendar_display
from khal.compat import to_unicode
from khal.khalendar.exceptions import ReadOnlyCalendarError
from khal.exceptions import FatalError
from khal.khalendar.exceptions import ReadOnlyCalendarError, DuplicateUid
from khal.exceptions import InvalidDate, FatalError
from khal.khalendar.event import Event
from khal.khalendar.backend import sort_key
from khal import __version__, __productname__
from khal.log import logger
from .terminal import colored, get_terminal_size, merge_columns
Expand Down Expand Up @@ -90,7 +95,7 @@ def get_agenda(collection, locale, dates=None,
if not isinstance(date, datetime.date) else date
for date in dates
]
except aux.InvalidDate as error:
except InvalidDate as error:
logging.fatal(error)
sys.exit(1)

Expand Down Expand Up @@ -154,6 +159,7 @@ def agenda(collection, date=None, firstweekday=0, encoding='utf-8',

def new_from_string(collection, conf, date_list, location=None, repeat=None,
until=None):
"""construct a new event from a string and add it"""
try:
event = aux.construct_event(
date_list,
Expand All @@ -163,10 +169,7 @@ def new_from_string(collection, conf, date_list, location=None, repeat=None,
**conf['locale'])
except FatalError:
sys.exit(1)
event = Event(event,
collection.default_calendar_name,
locale=conf['locale'],
)
event = Event(event, collection.default_calendar_name, locale=conf['locale'])

try:
collection.new(event)
Expand All @@ -182,6 +185,7 @@ def new_from_string(collection, conf, date_list, location=None, repeat=None,


def interactive(collection, conf):
"""start the interactive user interface"""
from . import ui
pane = ui.ClassicView(collection,
conf,
Expand All @@ -191,3 +195,41 @@ def interactive(collection, conf):
pane, pane.cleanup,
program_info='{0} v{1}'.format(__productname__, __version__)
)


def import_ics(collection, conf, ics, batch=False, random_uid=False):
"""
:param batch: setting this to True will insert without asking for approval,
even when an event with the same uid already exists
:type batch: bool
"""
cal = icalendar.Calendar.from_ical(ics)
events = [item for item in cal.walk() if item.name == 'VEVENT']
events_grouped = defaultdict(list)
for event in events:
events_grouped[event['UID']].append(event)

vevents = list()
for uid in events_grouped:
vevents.append(sorted(events_grouped[uid], key=sort_key))
for vevent in vevents:
for sub_event in vevent:
event = Event(sub_event, calendar=collection.default_calendar_name,
locale=conf['locale'])
if not batch:
echo(event.long())
if batch or confirm("Do you want to import this event into `{}`?"
"".format(collection.default_calendar_name)):
ics = aux.ics_from_list(vevent, random_uid)
try:
collection.new(
Item(ics.to_ical().decode('utf-8')),
collection=collection.default_calendar_name)
except DuplicateUid:
if batch or confirm("An event with the same UID already exists. "
"Do you want to update it?"):
collection.force_update(
Item(ics.to_ical().decode('utf-8')),
collection=collection.default_calendar_name)
else:
logger.warn("Not importing event with UID `{}`".format(event.uid))
4 changes: 4 additions & 0 deletions khal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ class UnsupportedFeatureError(Error):

"""something Failed but we know why"""
pass


class InvalidDate(Error):
pass
29 changes: 16 additions & 13 deletions khal/khalendar/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@
THISANDPRIOR = 'THISANDPRIOR'


def sort_key(vevent):
# insert the (sub) events in the right order, e.g. recurrence-id events
# after the corresponding rrule event
assert isinstance(vevent, icalendar.Event) # REMOVE ME
uid = str(vevent['UID'])
rec_id = vevent.get(RECURRENCE_ID)
if rec_id is None:
return uid, 0
rrange = rec_id.params.get('RANGE')
if rrange == THISANDFUTURE:
return uid, aux.to_unix_time(rec_id.dt)
else:
return uid, 1


class SQLiteDb(object):
"""
This class should provide a caching database for a calendar, keeping raw
Expand Down Expand Up @@ -223,25 +238,13 @@ def update(self, vevent, href, etag=''):
"""
if href is None:
raise ValueError('href may not be None')
ical = icalendar.Event.from_ical(vevent)

if isinstance(vevent, icalendar.cal.Event):
ical = vevent
else:
ical = icalendar.Event.from_ical(vevent)

# insert the (sub) events in the right order, e.g. recurrence-id events
# after the corresponding rrule event
def sort_key(vevent):
assert isinstance(vevent, icalendar.Event) # REMOVE ME
uid = vevent['UID']
rec_id = vevent.get(RECURRENCE_ID)
if rec_id is None:
return uid, 0
rrange = rec_id.params.get('RANGE')
if rrange == THISANDFUTURE:
return uid, aux.to_unix_time(rec_id.dt)
else:
return uid, 1
vevents = (aux.sanitize(c) for c in ical.walk() if c.name == 'VEVENT')
# Need to delete the whole event in case we are updating a
# recurring event with an event which is either not recurring any
Expand Down
6 changes: 6 additions & 0 deletions khal/khalendar/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,9 @@ class UnsupportedRecursion(Error):

"""raised if the RRULE is not understood by dateutil.rrule"""
pass


class DuplicateUid(Error):

"""an event with this UID already exists"""
existing_href = None
62 changes: 50 additions & 12 deletions khal/khalendar/khalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,19 @@
khalendar.Calendar and CalendarCollection should be a nice, abstract interface
to a calendar (collection). Calendar operates on vdirs but uses an sqlite db
for caching (see backend if you're interested).
If you want to see how the sausage is made:
Welcome to the sausage factory!
"""
import datetime
import os
import os.path

from vdirsyncer.storage.filesystem import FilesystemStorage
from vdirsyncer.exceptions import AlreadyExistingError

from . import backend
from .event import Event
from .. import log
from .exceptions import CouldNotCreateDbDir, UnsupportedFeatureError, \
ReadOnlyCalendarError, UpdateFailed
ReadOnlyCalendarError, UpdateFailed, DuplicateUid

logger = log.logger

Expand Down Expand Up @@ -129,8 +127,8 @@ def get_events_at(self, dtime=datetime.datetime.now()):
def get_event(self, href):
return self._cover_event(self._dbtool.get(href))

def update(self, event):
"""update an event in the database
def update(self, event, force=False):
"""update an event in vdir storage and in the database
param event: the event that should be updated
type event: event.Event
Expand All @@ -144,19 +142,52 @@ def update(self, event):
self._dbtool.update(event.vevent.to_ical(), event.href, event.etag)
self._dbtool.set_ctag(self.local_ctag())

def force_update(self, event):
# FIXME after the next vdirsyncer release, that check function is
# not needed than
# AlreadyExistingError now knows the conflicting events uid
def check(self, item):
"""check if this an event with this item's uid already exists"""
try:
# FIXME remove on next vdirsyncer release
href = self._deterministic_href(item)
except AttributeError:
href = self._get_href(item.uid)
if not self.has(href):
return None, None
else:
return href, self.get(href)[1]

if self._readonly:
raise ReadOnlyCalendarError()
with self._dbtool.at_once():
href, etag = check(self._storage, event)
if href is None:
self.new(event)
else:
etag = self._storage.update(href, event, etag)
self._dbtool.update(event.raw, href, etag)
self._dbtool.set_ctag(self.local_ctag())

def new(self, event):
"""save a new event to the database
"""save a new event to the vdir and the database
param event: the event that should be updated
type event: event.Event
"""
assert not event.etag
if hasattr(event, 'etag'):
assert not event.etag
if self._readonly:
raise ReadOnlyCalendarError()

with self._dbtool.at_once():
event.href, event.etag = self._storage.upload(event)
self._dbtool.update(event.to_ical(), event.href, event.etag)

try:
href, etag = self._storage.upload(event)
except AlreadyExistingError as Error:
href = getattr(Error, 'existing_href', None)
raise DuplicateUid(href)
self._dbtool.update(event.raw, href, etag)
self._dbtool.set_ctag(self.local_ctag())

def delete(self, href, etag):
Expand Down Expand Up @@ -195,8 +226,8 @@ def db_update(self):
self._dbtool.set_ctag(self.local_ctag())

def _update_vevent(self, href):
"""should only be called during db_update, does not check for
readonly"""
"""should only be called during db_update, only updates the db,
does not check for readonly"""
event, etag = self._storage.get(href)
try:
self._dbtool.update(event.raw, href=href, etag=etag)
Expand Down Expand Up @@ -248,6 +279,7 @@ def default_calendar_name(self):
return names[0]

def append(self, calendar):
"""append a new khalendar to this collection"""
self._calnames[calendar.name] = calendar

def get_allday_by_time_range(self, start):
Expand All @@ -273,6 +305,12 @@ def get_events_at(self, dtime=datetime.datetime.now()):
def update(self, event):
self._calnames[event.calendar].update(event)

def force_update(self, event, collection=None):
if collection:
self._calnames[collection].force_update(event)
else:
self._calnames[event.calendar].force_update(event)

def new(self, event, collection=None):
if collection:
self._calnames[collection].new(event)
Expand Down
Loading

0 comments on commit 514a5b1

Please sign in to comment.