At 22:50 on 29 Dec 2010, Nick Drage wrote:
> Is there anyone here who's converted to Remind/Wyrd from using the
> Palm calendar application? I'm currently looking to convert my Palm
> calendar to .rem format via jpilot and ical2rem.pl, but finding the
> conversion process "non trivial", so would appreciate lessons learnt
> from anyone who has gone before me.
Not this exactly, but I did write a python script to synchronise remind
with an ics file (actually from a Sony Ericsson phone). Mainly used the
python vobject module to parse the ics file - you may find the attached
scripts useful. I don't use these anymore as I sync with Google
Calendar on an Android phone using https://github.com/markk/gsync
Cheers,
Mark
--
Mark Knoop
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
A python script to compare two ics files. Identifies and prints changes in
a custom format.
TODO:
- move lots of strings to config
- enable config from file
- detect dos/unix lineendings
"""
import codecs, logging, locale, sys
import vobject, datetime, calendar
from optparse import OptionParser
from dateutil.tz import *
Version = "0.1alpha"
# wrap stdout in StreamWriter
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
# defaults
class Config():
def __init__(self):
pass
config = Config()
config.primarykey = 'x-irmc-luid' # primary key to compare files
config.newline = '$newline$' # string to replace \n
config.ignorefields = ['categories', 'uid', 'status', 'last-modified']
# list of fields to ignore when
# comparing events
config.modeline = '# vim: set ft=remind textwidth=95 nospell:\n'
# first line of output
config.defaultcategory = 'personal' # default category for events
# read commandline switches
usage = 'usage: %prog [options] [file1] [file2]'
parser = OptionParser(usage=usage, version='%prog ' + Version)
parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
default=False, help='print debugging info')
(options, args) = parser.parse_args()
# set logging level
if options.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
# output
class RemindOut():
def __init__(self):
# lists of events
self.deletions = []
self.additions = []
self.changedto = []
# list of change details
self.changes = []
def prettyPrint(self):
""" print events in custom format """
printmodeline = False
if len(self.deletions):
printmodeline = True
print '# these events were deleted'
for e in self.deletions:
print formatevent(e)
print
if len(self.additions):
printmodeline = True
print '# these events were added'
for e in self.additions:
print formatevent(e)
print
if len(self.changedto):
print '# these events were changed'
for c, e in zip(self.changes, self.changedto):
print c,
print formatevent(e)
print
if printmodeline:
print config.modeline
remindout = RemindOut()
def formatevent(event):
""" format event """
logging.debug(event)
startdatetime = event['dtstart'][0].value.strftime('%b %d %Y AT %H:%M')
dur_seconds = (event['dtend'][0].value - event['dtstart'][0].value).seconds
hours, remainder = divmod(dur_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
duration = '%02d:%02d' % (hours, minutes)
try:
if event['categories'] is not None:
category = event['categories'][0].value
else:
category = config.defaultcategory
except KeyError:
category = config.defaultcategory
if event['summary'] is not None:
summary = event['summary'][0].value
try:
if event['location'] is not None:
summary += ' at ' + event['location'][0].value
except KeyError:
pass
try:
if event['description'] is not None:
summary += '|\\\n' + event['description'][0].value
except KeyError:
pass
return 'REM %s DURATION %s TAG %s \\\n\tMSG %%g %%3 %%"%s%%"%%' % \
(startdatetime, duration, category, summary)
def openfile(filename):
""" open ical file, return ical object """
try:
fileIO = codecs.open(filename, 'r', 'utf-8')
filedata = fileIO.readlines()
fileIO.close()
except IOError:
raise Exception, 'Cannot open file.'
# remove ignored fields and replace \r\n
icaldata = ''
for l in filedata:
if l.split(':')[0].lower() in config.ignorefields:
continue
icaldata += l.replace('\n', config.newline).replace(\
'\r%s' % config.newline, '\n')
try:
ical = vobject.readOne(icaldata)
except:
raise Exception, 'Cannot parse ical data.'
return ical
def fielddiff(field_orig, field_sync):
""" compare two fields """
logging.debug(field_orig)
logging.debug(field_sync)
if field_orig is None:
fieldname = field_sync[0].name
orig = None
else:
fieldname = field_orig[0].name
orig = field_orig[0].value
if field_sync is None:
sync = None
else:
try:
sync = field_sync[0].value
except AttributeError:
logging.info("This is bug 1")
logging.info(field_sync)
sync = None
return '# %s changed\n# from: %s\n# to: %s\n' \
% (fieldname, orig, sync)
def resolve(event_orig, event_sync):
""" parse differences between events """
if event_sync is None:
# event deleted
remindout.deletions.append(event_orig)
elif event_orig is None:
# new event
remindout.additions.append(event_sync)
else:
# event changed
remindout.changedto.append(event_sync)
changes = ''
# ensure both events have all keys
for field in event_orig.keys():
if field not in event_sync.keys():
event_sync[field] = None
for field in event_sync.keys():
if field not in event_orig.keys():
event_orig[field] = None
if not event_orig[field] == event_sync[field]:
logging.debug(event_orig)
logging.debug(event_sync)
changes += fielddiff(event_orig[field], event_sync[field])
remindout.changes.append(changes)
def execute():
ical0 = openfile(args[0])
ical1 = openfile(args[1])
# create ical1 dictionary keyed by primary key
ical1dict = {}
for event in ical1.vevent_list:
try:
ical1dict[event.contents[config.primarykey][0].value] = event.contents
except KeyError:
raise Exception, 'Primary key does not exist.'
for event in ical0.vevent_list:
# find corresponding event in ical1
try:
ekey = event.contents[config.primarykey][0].value
except KeyError:
raise Exception, 'Primary key does not exist.'
if not ekey in ical1dict:
# event has been deleted
resolve(event.contents, None)
elif not event.contents == ical1dict[ekey]:
# event has been changed
resolve(event.contents, ical1dict[ekey])
del ical1dict[ekey]
else:
# event is unchanged
del ical1dict[ekey]
# deal with remaining (new) events in ical1
for ekey in ical1dict.keys():
resolve(None, ical1dict[ekey])
# print output
remindout.prettyPrint()
if __name__ == '__main__':
if not len(args) == 2:
raise Exception, 'Incorrect usage.'
execute()
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
A python script to convert standard input from remind to:
- LaTeX
- iCalendar (RFC2445)
- Google Calendar (gdata)
http://code.google.com/apis/calendar/data/1.0/developers_guide_python.html
Designed to take input from:
rem -s
i.e:
yyyy/mm/dd special tag dur time body
- special is ignored
- tags are comma-separated and become iCalendar CATEGORIES, except for a tag
containing "=" which are parsed into particular data pairs
- recognized tags:
- TZ becomes iCalendar timezone
- TRANSP={OPAQUE|TRANSPARENT} (default OPAQUE)
- dur becomes iCalendar DURATION
- time becomes starttime
- yyyy/mm/dd starttime become iCalendar DTSTART
- body is split by "|"
- time is removed from front of line if present (crude test)
- first line is split by " at " to become iCalendar SUMMARY and LOCATION
- remaining lines become iCalendar DESCRIPTION
TODO:
- read a config file ~/.rempy/config ConfigParser module
- get all day events first each day in LaTeX (why does rem put them last?)
- .ics output obeys from and to options
- use rem -l option to get file info line
# fileinfo <lineno> <filename>
include this info in X-REMIND-FILEINFO
not difficult, but would it be useful?
perhaps, if it can also be added into a gevent
- rem breaks a multi-day event into multiple all day events
try to join these back together
"""
import os, sys, locale, codecs, logging
import vobject, datetime, calendar
import hashlib
from optparse import OptionParser
from dateutil.tz import *
Version = "0.1alpha"
# defaults
class Config():
def __init__(self):
pass
config = Config()
config.remnewlinechar = '|' # newline token in remind
config.remlocation = ' at ' # token denoting location in remind
config.fwd = 0 # first weekday 0=Monday, 6=Sunday
config.timeformat = '%H:%M' # strftime format for appointment times
config.timeseparator = '--' # timespan separator
config.minicalmonths = 3 # number of months to show
config.datespanformat_d = '%-d' # format for datespan, date only
config.datespanformat_dm = '%-d %B' # format for datespan, date and month only
config.datespanformat_dmy = '%-d %B %Y' # format for datespan, complete
config.datespanseparator = ' -- ' # datespan separator
config.dayheadfield1 = '%-d %B' # format for first field of day header
config.dayheadfield2 = '%A' # format for second field of day header
config.latextemplate = 'calendar.tpl' # name of latex template file
config.configpath = ['.', '~/.rempy', '/etc/rempy'] # files are looked for in these
# places (in order)
# read commandline switches
usage = 'usage: rem -s[months|+weeks] [dd mmmm yyyy] | %prog [options]'
parser = OptionParser(usage=usage, version='%prog ' + Version)
parser.add_option('-o', '--output', dest='output', metavar='FILE',
default='calendar', help='basename of the output file')
parser.add_option('-z', '--timezone', dest='timezone', metavar='Region/City',
default=None, help='set the calendar timezone (default=local)')
# TODO should this be homezone (i.e. GMT)? to deal with Summer Time adjustment?
parser.add_option('-u', '--utc', action='store_true', dest='utc',
default=False, help='adjust all appointments to UTC')
parser.add_option('-x', '--noical', action='store_false', dest='ical',
default=True, help='do not output iCalendar file (default=True)')
parser.add_option('-l', '--latex', action='store_true', dest='latex',
default=False, help='output LaTeX file (default=False)')
parser.add_option('-f', '--from', dest='fr', metavar='YYYYMMDD',
default=None, help='output LaTeX from date')
parser.add_option('-t', '--to', dest='to', metavar='YYYYMMDD',
default=None, help='output LaTeX to date')
parser.add_option('-s', '--shorten', action='store_true', dest='shortenallday',
default=False, help='shorten all day appointments (00:00-23:59)')
parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
default=False, help='print debugging info')
(options, args) = parser.parse_args()
# set logging level
if options.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
# functions
def addevent(cal, app):
""" converts a dictionary to a vobject event and adds it to the calendar
object """
evt = cal.add('vevent')
evt.add('uid').value = app['uid']
# parse date, time, timezone
if app['timezone']:
try:
tz = tzfile('/usr/share/zoneinfo/%s' % app['timezone'])
except IOError:
logging.warning('No such timezone file %s' % app['timezone'])
tz = tzlocal()
else:
tz = tzlocal()
try:
start = datetime.datetime(app['date'][0], app['date'][1],
app['date'][2], app['time'][0], app['time'][1], tzinfo=tz)
except TypeError:
logging.critical('Unable to parse datetime! Aborting.\n%s %s' % \
(app['date'], app['time']))
raise
# adjust to UTC
if options.utc:
start = (start - start.utcoffset()).replace(tzinfo=None)
# duration
if app['dur']:
# use dtend - seems to be better supported by other apps
dtend = start + datetime.timedelta(minutes=app['dur'])
elif start.hour == 0 and start.minute == 0:
# all day event - TODO this is a guess...
if options.shortenallday:
dtend = start + datetime.timedelta(days=1, minutes=-1)
else:
# convert start to date
start = datetime.date(start.year, start.month, start.day)
dtend = start + datetime.timedelta(days=1)
# make all day events transparent
app['transp'] = 'TRANSPARENT'
else:
dtend = start
evt.add('dtstart').value = start
evt.add('dtend').value = dtend
# add summary, location, description, transparency
evt.add('summary').value = app['summary']
if app['location']:
evt.add('location').value = app['location']
if app['description']:
evt.add('description').value = app['description']
if len(app['categories']):
evt.add('categories').value = app['categories']
if app['transp']:
evt.add('transp').value = app['transp']
return cal
def texize(text, NewLines=False):
""" make replacements so text is safe for LaTeX """
texChars = {u'-':u'--', u'%':u'\\% ', u'$':u'\\$ ', u'£':u'\\pounds ',
u'#':u'\\# ', u'&':u'\\& ', u'_':u'\\_ ', u'...':u'\\ldots ',
u'{':u'\\{ ', u'}':u'\\} ', u'~':u'$\\sim$',
u'<':u'\\textlessthan ', u'>':u'\\textgreaterthan '}
TexString = text.replace(u'\\', u'\\textbackslash') # must do this one first!
TexString = TexString.replace('\\t', u' ') # remove any tabs
for character in texChars.keys():
TexString = TexString.replace(character, texChars[character])
if NewLines is False:
TexString = TexString.replace('\n', u' ')
elif NewLines is True:
TexString = TexString.replace('\n', u' \\newline\n')
return TexString
def makelatex(vevent):
""" parse an event into values for the LaTeX output """
apptemplate = '\\appointment{%(tsp)s}{%(sum)s}{%(cat)s}{%(loc)s}{%(des)s}{%(transp)s}\n'
event = {'tsp': ''}
# summary
event['sum'] = texize(vevent.summary.value)
logging.debug('processing %s' % event['sum'])
logging.debug(vevent)
# transparency
event['transp'] = vevent.transp.value
# categories
try:
event['cat'] = texize(', '.join(vevent.categories.value))
except AttributeError:
event['cat'] = ''
# location
try:
event['loc'] = texize(vevent.location.value)
except AttributeError:
event['loc'] = ''
# description
try:
event['des'] = texize(vevent.description.value, NewLines=True)
except AttributeError:
event['des'] = ''
# timespan
stime = vevent.dtstart.value
etime = vevent.dtend.value
if stime + datetime.timedelta(days=1) == etime and \
stime.hour == 0 and stime.minute == 0:
# all day event
event['tsp'] = ''
elif stime == etime:
# no duration event
event['tsp'] = stime.strftime(config.timeformat)
elif stime.date() == etime.date():
# ends on the same day
event['tsp'] = '%s%s%s' % (stime.strftime(config.timeformat),
config.timeseparator, etime.strftime(config.timeformat))
else:
# ends on a different day
diff = etime.date() - stime.date()
event['tsp'] = '%s--%s+%s' % (stime.strftime(config.timeformat),
etime.strftime(config.timeformat), diff.days)
return apptemplate % event
def parsedatestring(yyyymmdd):
""" convert a string to a date object if possible """
if yyyymmdd is None: return None
if type(yyyymmdd) == str or type(yyyymmdd) == unicode:
if len(yyyymmdd) != 8:
logging.warning('Ignoring invalid date %s.' % yyyymmdd)
return None
else:
# parse yyyymmdd
try:
return datetime.date(int(yyyymmdd[0:4]),
int(yyyymmdd[4:6]), int(yyyymmdd[6:8]))
except ValueError, TypeError:
logging.warning('Ignoring invalid date %s.' % yyyymmdd)
return None
def latex3months(year, month):
""" return a LaTeX string of a table of next three months """
calendar.setfirstweekday(config.fwd)
head = u'\\minicalhead'
def nozeros(d):
if d == 0:
return ''
else:
return str(d)
weeks = [calendar.weekheader(1).split() * config.minicalmonths]
for m in range(config.minicalmonths):
head += datetime.date(year, month, 1).strftime('{%B}{%Y}')
thismonth = calendar.monthcalendar(year, month)
while len(thismonth) < 6:
thismonth.append([0] * 7)
for w in range(6):
if m == 0:
weeks.append([])
weeks[w + 1] += [nozeros(d) for d in thismonth[w]]
if month == 12:
month = 1
year += 1
else:
month += 1
if weeks[-1] == [''] * (config.minicalmonths * 7):
del weeks[-1]
weekstring = ''
for w in weeks:
weekstring += ' & '.join(w) + ' \\\\\n'
return (head + '\n', weekstring)
# validate calendar range
options.fr = parsedatestring(options.fr)
options.to = parsedatestring(options.to)
# read stdin
encoding = locale.getpreferredencoding()
logging.debug('Encoding is %s' % encoding)
appointments = codecs.getreader(encoding)(sys.stdin)
# initialize list of processed appointments (list of dicts)
apps = []
# initialize iCalendar object
cal = vobject.iCalendar()
cal.add('prodid').value = "-//REMPY//PYVOBJECT//NONSGML Version 1//EN"
for appointment in appointments: #[0:5]:
# strip trailing whitespace and newline
appointment = appointment.rstrip()
logging.debug('%s' % appointment)
# make md5 digest
uid = hashlib.md5(appointment.encode(encoding)).hexdigest()
# split into fields
remfields = appointment.split(None, 5)
(year, month, day) = remfields[0].split('/')
# check if appointment is in range
appstart = parsedatestring('%s%s%s' % (year, month, day))
logging.debug('DTSTART: %s (%s, %s, %s)' % (appstart, year, month, day))
if options.fr is not None:
if appstart < options.fr:
continue
if options.to is not None:
if appstart > options.to:
continue
app = {'date': (int(year), int(month), int(day)), 'uid': uid}
# ignore remfields[1] special
# tags
if remfields[2] == '*':
app['tags'] = []
else:
app['tags'] = remfields[2].split(',')
# parse tags
app['timezone'] = options.timezone
app['transp'] = 'OPAQUE'
app['categories'] = []
for tag in app['tags']:
if '=' in tag:
(key, value) = tag.split('=')
if key == 'TZ':
app['timezone'] = value
elif key == 'TRANSP':
app['transp'] = value
else:
logging.warning('Unknown tag pair: %s' % tag)
else:
app['categories'].append(tag)
# dur
if remfields[3] == '*':
app['dur'] = None
else:
app['dur'] = int(remfields[3])
# time
if remfields[4] == '*':
app['time'] = (0, 0)
else:
(hour, minute) = (int(remfields[4]) // 60, int(remfields[4]) % 60)
app['time'] = (hour, minute)
# body - remove leading time
# first test for 24hr time: hh:mm-hh:mm
# then for 12hr time: will end in am, pm, or m+x
timespan = remfields[5].split(None, 1)[0]
if len(timespan) < 6 or timespan == 'Avraham':
pass
# TODO fix this time test - catches e.g. "Avraham"
elif (timespan[2] == ':' and timespan[5] == '-' and timespan[8] == ':') \
or (timespan.endswith('pm') or timespan.endswith('am') or \
timespan[-3:-1] == 'm+'):
remfields[5] = remfields[5].split(None, 1)[1]
# body - split into lines
remfields[5] = remfields[5].split(config.remnewlinechar)
# - split first line
sumloc = remfields[5][0].split(config.remlocation)
app['summary'] = sumloc[0]
if len(sumloc) == 1:
app['location'] = None
elif len(sumloc) == 2:
app['location'] = sumloc[1]
else:
logging.warning('Too many fields in body! %s' % sumloc)
# join remaining lines into description
if len(remfields[5]) > 1:
app['description'] = '\n'.join(remfields[5][1:])
else:
app['description'] = None
logging.debug('%s' % app)
# add appointment to calendar
cal = addevent(cal, app)
# add appointment to list
apps.append(app)
# output
# serialize calendar
icalstream = cal.serialize()
# set range fr/to if None
if options.fr is None:
# use date of first event
if type(cal.vevent_list[0].dtstart.value) == datetime.date:
options.fr = cal.vevent_list[0].dtstart.value
else:
options.fr = cal.vevent_list[0].dtstart.value.date()
if options.to is None:
# use date of last event
if type(cal.vevent_list[-1].dtstart.value) == datetime.date:
options.to = cal.vevent_list[-1].dtstart.value
else:
options.to = cal.vevent_list[-1].dtstart.value.date()
# outputs
if options.ical:
# output calendar to file
icalout = codecs.open('%s.ics' % options.output, 'w', encoding)
icalout.write(icalstream.decode(encoding))
icalout.close()
if options.latex:
# format calendar period
if options.fr.year == options.to.year:
if options.fr.month == options.to.month:
pfr = options.fr.strftime(config.datespanformat_d)
else:
pfr = options.fr.strftime(config.datespanformat_dm)
else:
pfr = options.fr.strftime(config.datespanformat_dmy)
pto = options.to.strftime(config.datespanformat_dmy)
calperiod = '%s%s%s' % (pfr, config.datespanseparator, pto)
logging.debug('calendar period: %s' % calperiod)
# make calendar covering date range
currentday = options.fr
days = ''
appsiter = iter(cal.vevent_list)
try:
currentapp = appsiter.next()
currentappdate = currentapp.dtstart.value.date()
except StopIteration:
logging.warning('No appointments in this calendar.')
currentappdate = options.to + datetime.timedelta(1)
while currentday <= options.to:
logging.debug('processing day %s, app %s' % (currentday, currentappdate))
thisday = currentday.strftime('\\dayhead{%s}{%s}\n' % \
(config.dayheadfield1, config.dayheadfield2))
if currentappdate > currentday:
days += thisday
currentday += datetime.timedelta(1)
continue
elif currentappdate < currentday:
try:
currentapp = appsiter.next()
currentappdate = currentapp.dtstart.value.date()
logging.debug('next app %s' % currentappdate)
except StopIteration:
currentappdate = options.to + datetime.timedelta(1)
continue
while currentappdate == currentday:
thisday += makelatex(currentapp)
try:
currentapp = appsiter.next()
currentappdate = currentapp.dtstart.value.date()
logging.debug('next app %s' % currentappdate)
except StopIteration:
currentappdate = options.to + datetime.timedelta(1)
days += thisday
currentday += datetime.timedelta(1)
# make minicalendar
(minicalhead, minicalweeks) = latex3months(options.fr.year,
options.fr.month)
# substitute appointments into calendar
foundtemplate = False
for p in config.configpath:
templatefilename = '%s/%s' % (os.path.expanduser(p),
config.latextemplate)
if os.path.isfile(templatefilename):
foundtemplate = True
break
if foundtemplate is False:
logging.critical('Cannot find template file!')
caltemplatefile = codecs.open(templatefilename, 'r', encoding)
caltemplate = caltemplatefile.read()
caltemplatefile.close()
callatex = caltemplate.replace('__CALPERIOD__', calperiod)
callatex = callatex.replace('__DAYS__', days)
callatex = callatex.replace('__MINICALHEAD__', minicalhead)
callatex = callatex.replace('__MINICALWEEKS__', minicalweeks)
# output latex to file
callatexout = codecs.open('%s.tex' % options.output, 'w', encoding)
callatexout.write(callatex) #.decode(encoding))
callatexout.close()
_______________________________________________
Remind-fans mailing list
[email protected]
http://lists.roaringpenguin.com/cgi-bin/mailman/listinfo/remind-fans
Remind is at http://www.roaringpenguin.com/products/remind