RE: Problem with UNICODE filenames still not fixed :( (part 1 of 3)
"Simon C" <[email protected]>
| Newsgroups | gmane.comp.mobile.bitpim.devel |
|---|---|
| Message-ID | <000001c5cc3b$57074c40$6400a8c0@HOME> |
Joe, There was more work than I thought before. Functions str(), access() and execfile() will not take filenames with non-ascii characters. I fixed it so that the path to any file used by bitpim can contain non-ascii characters, if the actual filename contains non-ascii there could still be problems so if you tried to add an image with non-ascii characters in the filename it would not work, I don't think this is a big limitation as I think the phones only work with ascii anyway. 11 files were touched, I've attached diffs as well. This is part 1 of 3, as it exceeds the max sourceforge e-mail size. If the person who raised this in the first place wants to try it out send me an e-mail. Simon
bpcalendar.py
(text/plain, 50.5 KB)
#!/usr/bin/env python ### BITPIM ### ### Copyright (C) 2003-2004 Roger Binns <[email protected]> ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the BitPim license as detailed in the LICENSE file. ### ### $Id: bpcalendar.py,v 1.31 2005/08/15 22:54:35 djpham Exp $ """Calendar user interface and data for bitpim. This module has a bp prefix so it doesn't clash with the system calendar module Version 3: The format for the calendar is standardised. It is a dict with the following fields: (Note: hour fields are in 24 hour format) 'string id': CalendarEntry object. CalendarEntry properties: description - 'string description' location - 'string location' priority - None=no priority, int from 1-10, 1=highest priority alarm - how many minutes beforehand to set the alarm (use 0 for on-time, None or -1 for no alarm) allday - True for an allday event, False otherwise start - (year, month, day, hour, minute) as integers end - (year, month, day, hour, minute) as integers serials - list of dicts of serials. repeat - None, or RepeatEntry object id - string id of this object. Created the same way as bpserials IDs for phonebook entries. notes - string notes categories - [ { 'category': string category }, ... ] ringtone - string ringtone assignment wallpaper - string wallpaper assignment. vibrate - True if the alarm is set to vibrate, False otherwise voice - ID of voice alarm CalendarEntry methods: get() - return a copy of the internal dict get_db_dict()- return a copy of a database.basedataobject dict. set(dict) - set the internal dict with the supplied dict set_db_dict(dict) - set internal data with the database.basedataobject dict is_active(y, m, d) - True if this event is active on (y,m,d) suppress_repeat_entry(y,m,d) - exclude (y,m,d) from this repeat event. RepeatEntry properties: repeat_type - one of daily, weekly, monthly, or yearly. interval - for daily: repeat every nth day. For weekly, for every nth week. dow - bitmap of which day of week are being repeated. suppressed - list of (y,m,d) being excluded from this series. -------------------------------------------------------------------------------- Version 2: The format for the calendar is standardised. It is a dict with the following fields: (Note: hour fields are in 24 hour format) start: - (year, month, day, hour, minute) as integers end: - (year, month, day, hour, minute) as integers # if you want no end, set to the same value as start, or to the year 4000 repeat: - one of None, "daily", "monfri", "weekly", "monthly", "yearly" description: - "String description" changeserial: - Set to integer 1 snoozedelay: - Set to an integer number of minutes (default 0) alarm: - how many minutes beforehand to set the alarm (use 0 for on-time, None for no alarm) daybitmap: - default 0, it will become which days of the week weekly events happen on (eg every monday and friday) ringtone: - index number of the ringtone for the alarm (use 0 for none - will become a string) pos: - integer that should be the same as the dictionary key for this entry exceptions: - (optional) A list of (year,month,day) tuples that repeats are suppressed """ # Standard modules import os import copy import calendar import datetime import random import sha import time import webbrowser # wx stuff import wx import wx.lib import wx.lib.masked.textctrl import wx.lib.intctrl # my modules import bphtml import bptime import calendarcontrol import calendarentryeditor import common import database import guihelper import helpids import pubsub import today import xyaptu #------------------------------------------------------------------------------- class CalendarDataObject(database.basedataobject): """ This class is a wrapper class to enable CalendarEntry object data to be stored in the database stuff. Once the database module is updated, this class will also be updated and eventually replace CalendarEntry. """ _knownproperties=['description', 'location', 'priority', 'alarm', 'notes', 'ringtone', 'wallpaper', 'start', 'end', 'vibrate', 'voice' ] _knownlistproperties=database.basedataobject._knownlistproperties.copy() _knownlistproperties.update( { 'repeat': ['type', 'interval', 'dow'], 'suppressed': ['date'], 'categories': ['category'] }) def __init__(self, data=None): if data is None or not isinstance(data, CalendarEntry): # empty data, do nothing return self.update(data.get_db_dict()) calendarobjectfactory=database.dataobjectfactory(CalendarDataObject) #------------------------------------------------------------------------------- class RepeatEntry(object): # class constants daily='daily' weekly='weekly' monthly='monthly' yearly='yearly' _interval=0 _dow=1 _dom=0 _moy=1 _dow_names=( {1: 'Sun'}, {2: 'Mon'}, {4: 'Tue'}, {8: 'Wed'}, {16: 'Thu'}, {32: 'Fri'}, {64: 'Sat'}) # this faster than log2(x) _dow_num={ 1: wx.DateTime.Sun, 2: wx.DateTime.Mon, 4: wx.DateTime.Tue, 8: wx.DateTime.Wed, 16: wx.DateTime.Thu, 32: wx.DateTime.Fri, 64: wx.DateTime.Sat } dow_names={ 'Sun': 1, 'Mon': 2, 'Tue': 4, 'Wed': 8, 'Thu': 16, 'Fri': 32, 'Sat': 64 } def __init__(self, repeat_type=daily): self._type=repeat_type self._data=[0,0] self._suppressed=[] def get(self): # return a dict representing internal data # mainly used for populatefs r={} if self._type==self.daily: r[self.daily]= { 'interval': self._data[self._interval] } elif self._type==self.weekly: r[self.weekly]= { 'interval': self._data[self._interval], 'dow': self._data[self._dow] } elif self._type==self.monthly: r[self.monthly]={ 'interval': self._data[self._interval], 'dow': self._data[self._dow] } else: r[self.yearly]=None s=[] for n in self._suppressed: s.append(n.get()) r['suppressed']=s return r def get_db_dict(self): # return a copy of the dict compatible with the database stuff db_r={} r={} r['type']=self._type if self._type==self.daily: r['interval']=self._data[self._interval] elif self._type==self.weekly or self._type==self.monthly: r['interval']=self._data[self._interval] r['dow']=self._data[self._dow] # and the suppressed stuff s=[] for n in self._suppressed: s.append({ 'date': n.iso_str(True) }) db_r['repeat']=[r] if len(s): db_r['suppressed']=s return db_r def set(self, data): # setting data from a dict, mainly used for getfromfs if data.has_key(self.daily): # daily type self.repeat_type=self.daily self.interval=data[self.daily]['interval'] elif data.has_key(self.weekly): # weekly type self.repeat_type=self.weekly self.interval=data[self.weekly]['interval'] self.dow=data[self.weekly]['dow'] elif data.has_key(self.monthly): self.repeat_type=self.monthly self.dow=data[self.monthly].get('dow', 0) self.interval=data[self.monthly].get('interval', 0) else: self.repeat_type=self.yearly s=[] for n in data.get('suppressed', []): s.append(bptime.BPTime(n)) self.suppressed=s def set_db_dict(self, data): r=data.get('repeat', [{}])[0] self.repeat_type=r['type'] _dow=r.get('dow', 0) _interval=r.get('interval', 0) if self.repeat_type==self.daily: self.interval=_interval elif self.repeat_type==self.weekly or self.repeat_type==self.monthly: self.interval=_interval self.dow=_dow # now the suppressed stuff s=[] for n in data.get('suppressed', []): s.append(bptime.BPTime(n['date'])) self.suppressed=s def _check_daily(self, s, d): if self.interval: # every nth day return (int((d-s).days)%self.interval)==0 else: # every weekday return d.weekday()<5 def _check_weekly(self, s, d): # check if at least one day-of-week is specified, if not default to the # start date if self.dow==0: self.dow=1<<(s.isoweekday()%7) # check to see if this is the nth week day_of_week=d.isoweekday()%7 # Sun=0, ..., Sat=6 sun_0=s-datetime.timedelta(s.isoweekday()%7) sun_1=d-datetime.timedelta(day_of_week) if ((sun_1-sun_0).days/7)%self.interval: # wrong week return False # check for the right weekday return ((1<<day_of_week)&self.dow) != 0 def _check_monthly(self, s, d): if self.dow==0: # no weekday specified, implied nth day of the month return d.day==s.day else: # every interval-th dow-day (ie 1st Mon) of the month dt=wx.DateTime.Now() if self.interval<5: # nth *day of the month _nth=self.interval else: # last *day of the month _nth=-1 return dt.SetToWeekDay(self._dow_num[self.dow], _nth, month=d.month-1, year=d.year) and \ dt.GetDay()==d.day def _check_yearly(self, s, d): return d.month==s.month and d.day==s.day def is_active(self, s, d): # check in the suppressed list if bptime.BPTime(d) in self._suppressed: # in the list, not part of this repeat return False # determine if the date is active if self.repeat_type==self.daily: return self._check_daily(s, d) elif self.repeat_type==self.weekly: return self._check_weekly(s, d) elif self.repeat_type==self.monthly: return self._check_monthly(s, d) elif self.repeat_type==self.yearly: return self._check_yearly(s, d) else: return False def _get_type(self): return self._type def _set_type(self, repeat_type): if repeat_type in (self.daily, self.weekly, self.monthly, self.yearly): self._type = repeat_type else: raise AttributeError, 'type' repeat_type=property(fget=_get_type, fset=_set_type) def _get_interval(self): if self._type==self.yearly: raise AttributeError return self._data[self._interval] def _set_interval(self, interval): if self._type==self.yearly: raise AttributeError self._data[self._interval]=interval interval=property(fget=_get_interval, fset=_set_interval) def _get_dow(self): if self._type==self.yearly: raise AttributeError return self._data[self._dow] def _set_dow(self, dow): if self._type==self.yearly: raise AttributeError self._data[self._dow]=dow dow=property(fget=_get_dow, fset=_set_dow) def _get_dow_str(self): try: _dow=self.dow except AttributeError: return '' names=[] for l in self._dow_names: for k,e in l.items(): if k&_dow: names.append(e) return ';'.join(names) dow_str=property(fget=_get_dow_str) def _get_suppressed(self): return self._suppressed def _set_suppressed(self, d): if not isinstance(d, list): raise TypeError, 'must be a list of string or BPTime' if not len(d) or isinstance(d[0], bptime.BPTime): # empty list or already a list of BPTime self._suppressed=d elif isinstance(d[0], str): # list of 'yyyy-mm-dd' self._suppressed=[] for n in d: self._suppressed.append(bptime.BPTime(n.replace('-', ''))) def add_suppressed(self, y, m, d): self._suppressed.append(bptime.BPTime((y, m, d))) def get_suppressed_list(self): return [x.date_str() for x in self._suppressed] suppressed=property(fget=_get_suppressed, fset=_set_suppressed) def _get_suppressed_str(self): return ';'.join(self.get_suppressed_list()) suppressed_str=property(fget=_get_suppressed_str) #------------------------------------------------------------------------------- class CalendarEntry(object): # priority const priority_high=1 priority_normal=5 priority_low=10 # no end date no_end_date=(4000, 1, 1) def __init__(self, year=None, month=None, day=None): self._data={} # setting default values if day is not None: self._data['start']=bptime.BPTime((year, month, day)) self._data['end']=bptime.BPTime((year, month, day)) else: self._data['start']=bptime.BPTime() self._data['end']=bptime.BPTime() self._data['serials']=[] self._create_id() def get(self): r=copy.deepcopy(self._data, _nil={}) if self.repeat is not None: r['repeat']=self.repeat.get() r['start']=self._data['start'].iso_str() r['end']=self._data['end'].iso_str() return r def get_db_dict(self): # return a dict compatible with the database stuff r=copy.deepcopy(self._data, _nil={}) # adjust for start & end r['start']=self._data['start'].iso_str(self.allday) r['end']=self._data['end'].iso_str(self.allday) # adjust for repeat & suppressed if self.repeat is not None: r.update(self.repeat.get_db_dict()) # take out uneeded keys if r.has_key('allday'): del r['allday'] return r def set(self, data): self._data={} self._data.update(data) self._data['start']=bptime.BPTime(data['start']) self._data['end']=bptime.BPTime(data['end']) if self.repeat is not None: r=RepeatEntry() r.set(self.repeat) self.repeat=r # try to clean up the dict for k, e in self._data.items(): if e is None or e=='' or e==[]: del self._data[k] def set_db_dict(self, data): # update our data with dict return from database self._data={} self._data.update(data) # adjust for allday self.allday=len(data['start'])==8 # adjust for start and end self._data['start']=bptime.BPTime(data['start']) self._data['end']=bptime.BPTime(data['end']) # adjust for repeat if data.has_key('repeat'): rp=RepeatEntry() rp.set_db_dict(data) self.repeat=rp def is_active(self, y, m ,d): # return true if if this event is active on this date, # mainly used for repeating events. s=self._data['start'].date e=self._data['end'].date d=datetime.date(y, m, d) if d<s or d>e: # before start date, after end date return False if self.repeat is None: # not a repeat event, within range so it's good return True # repeat event: check if it's in range. return self.repeat.is_active(s, d) def suppress_repeat_entry(self, y, m, d): if self.repeat is None: # not a repeat entry, do nothing return self.repeat.add_suppressed(y, m, d) def _set_or_del(self, key, v, v_list=()): if v is None or v in v_list: if self._data.has_key(key): del self._data[key] else: self._data[key]=v def _get_description(self): return self._data.get('description', '') def _set_description(self, desc): self._set_or_del('description', desc, ('',)) description=property(fget=_get_description, fset=_set_description) def _get_location(self): return self._data.get('location', '') def _set_location(self, location): self._set_or_del('location', location, ('',)) location=property(fget=_get_location, fset=_set_location) def _get_priority(self): return self._data.get('priority', None) def _set_priority(self, priority): self._set_or_del('priority', priority) priority=property(fget=_get_priority, fset=_set_priority) def _get_alarm(self): return self._data.get('alarm', -1) def _set_alarm(self, alarm): self._set_or_del('alarm', alarm) alarm=property(fget=_get_alarm, fset=_set_alarm) def _get_allday(self): return self._data.get('allday', False) def _set_allday(self, allday): self._data['allday']=allday allday=property(fget=_get_allday, fset=_set_allday) def _get_start(self): return self._data['start'].get() def _set_start(self, datetime): self._data['start'].set(datetime) start=property(fget=_get_start, fset=_set_start) def _get_start_str(self): return self._data['start'].date_str()+' '+\ self._data['start'].time_str(False, '00:00') start_str=property(fget=_get_start_str) def _get_end(self): return self._data['end'].get() def _set_end(self, datetime): self._data['end'].set(datetime) end=property(fget=_get_end, fset=_set_end) def _get_end_str(self): return self._data['end'].date_str()+' '+\ self._data['end'].time_str(False, '00:00') end_str=property(fget=_get_end_str) def _get_vibrate(self): return self._data.get('vibrate', 0) def _set_vibrate(self, v): self._set_or_del('vibrate', v, (None, 0, False)) vibrate=property(fget=_get_vibrate, fset=_set_vibrate) def _get_voice(self): return self._data.get('voice', None) def _set_voice(self, v): self._set_or_del('voice', v, (None,)) voice=property(fget=_get_voice, fset=_set_voice) def _get_serials(self): return self._data.get('serials', None) def _set_serials(self, serials): self._data['serials']=serials serials=property(fget=_get_serials, fset=_set_serials) def _get_repeat(self): return self._data.get('repeat', None) def _set_repeat(self, repeat): self._set_or_del('repeat', repeat) repeat=property(fget=_get_repeat, fset=_set_repeat) def _get_id(self): s=self._data.get('serials', []) for n in s: if n.get('sourcetype', None)=='bitpim': return n.get('id', None) return None def _set_id(self, id): s=self._data.get('serials', []) for n in s: if n.get('sourcetype', None)=='bitpim': n['id']=id return self._data['serials'].append({'sourcetype': 'bitpim', 'id': id } ) id=property(fget=_get_id, fset=_set_id) def _get_notes(self): return self._data.get('notes', '') def _set_notes(self, s): self._set_or_del('notes', s, ('',)) notes=property(fget=_get_notes, fset=_set_notes) def _get_categories(self): return self._data.get('categories', []) def _set_categories(self, s): self._set_or_del('categories', s,([],)) if s==[] and self._data.has_key('categories'): del self._data['categories'] categories=property(fget=_get_categories, fset=_set_categories) def _get_categories_str(self): c=self.categories if len(c): return ';'.join([x['category'] for x in c]) else: return '' categories_str=property(fget=_get_categories_str) def _get_ringtone(self): return self._data.get('ringtone', '') def _set_ringtone(self, rt): self._set_or_del('ringtone', rt, ('',)) ringtone=property(fget=_get_ringtone, fset=_set_ringtone) def _get_wallpaper(self): return self._data.get('wallpaper', '',) def _set_wallpaper(self, wp): self._set_or_del('wallpaper', wp, ('',)) wallpaper=property(fget=_get_wallpaper, fset=_set_wallpaper) # we use two random numbers to generate the serials. _persistrandom # is seeded at startup _persistrandom=random.Random() def _create_id(self): "Create a BitPim serial for this entry" rand2=random.Random() # this random is seeded when this function is called num=sha.new() num.update(`self._persistrandom.random()`) num.update(`rand2.random()`) self._data["serials"].append({"sourcetype": "bitpim", "id": num.hexdigest()}) def _get_print_data(self): """ return a list of strings used for printing this event: [0]: start time, [1]: '', [2]: end time, [3]: Description [4]: Repeat Type, [5]: Alarm """ if self.allday: t0='All Day' t1='' else: t0=self._data['start'].time_str() t1=self._data['end'].time_str() rp=self.repeat if rp is None: rp_str='' else: rp_str=rp.repeat_type[0].upper() if self.alarm==-1: alarm_str='' else: alarm_str='%d:%02d'%(self.alarm/60, self.alarm%60) return [t0, '', t1, self.description, rp_str, alarm_str] print_data=property(fget=_get_print_data) def cmp_by_time(a, b): """ compare 2 objects by start times. -1 if a<b, 0 if a==b, and 1 if a>b allday is always less than having start times. Mainly used for sorting list of events """ if not isinstance(a, CalendarEntry) or \ not isinstance(b, CalendarEntry): raise TypeError, 'must be a CalendarEntry object' if a.allday and b.allday: return 0 if a.allday and not b.allday: return -1 if not a.allday and b.allday: return 1 t0=a.start[3:] t1=b.start[3:] if t0<t1: return -1 if t0==t1: return 0 if t0>t1: return 1 cmp_by_time=staticmethod(cmp_by_time) def _summary(self): # provide a one-liner summary string for this event if self.allday: str=self.description else: hr=self.start[3] ap="am" if hr>=12: ap="pm" hr-=12 if hr==0: hr=12 str="%2d:%02d %s" % (hr, self.start[4], ap) str+=" "+self.description return str summary=property(fget=_summary) #------------------------------------------------------------------------------- class Calendar(calendarcontrol.Calendar): """A class encapsulating the GUI and data of the calendar (all days). A seperate L{DayViewDialog} is used to edit the content of one particular day.""" CURRENTFILEVERSION=3 def __init__(self, mainwindow, parent, id=-1): """constructor @type mainwindow: gui.MainWindow @param mainwindow: Used to get configuration data (such as directory to save/load data. @param parent: Widget acting as parent for this one @param id: id """ self.mainwindow=mainwindow self.entrycache={} self.entries={} self.repeating=[] # nb this is stored unsorted self._data={} # the underlying data calendarcontrol.Calendar.__init__(self, parent, rows=5, id=id) self.dialog=calendarentryeditor.Editor(self) pubsub.subscribe(self.OnMediaNameChanged, pubsub.MEDIA_NAME_CHANGED) today.bind_notification_event(self.OnTodayItem, today.Today_Group_Calendar) def OnPrintDialog(self, mainwindow, config): dlg=CalendarPrintDialog(self, mainwindow, config) dlg.ShowModal() dlg.Destroy() def OnMediaNameChanged(self, msg): d=msg.data _type=d.get(pubsub.media_change_type, None) _old_name=d.get(pubsub.media_old_name, None) _new_name=d.get(pubsub.media_new_name, None) if _type is None or _old_name is None or _new_name is None: # invalid/incomplete data return if _type!=pubsub.wallpaper_type and \ _type!=pubsub.ringtone_type: # neither wallpaper nor ringtone return _old_name=common.basename(_old_name) _new_name=common.basename(_new_name) if _type==pubsub.wallpaper_type: attr_name='wallpaper' else: attr_name='ringtone' modified=False for k,e in self._data.items(): if getattr(e, attr_name, None)==_old_name: setattr(e, attr_name, _new_name) modified=True if modified: # changes were made, update everything self.updateonchange() def getdata(self, dict): """Return underlying calendar data in bitpim format @return: The modified dict updated with at least C{dict['calendar']}""" if dict.get('calendar_version', None)==2: # return a version 2 dict dict['calendar']=self._convert3to2(self._data, dict.get('ringtone-index', None)) else: dict['calendar']=copy.deepcopy(self._data, _nil={}) return dict def updateonchange(self): """Called when our data has changed The disk, widget and display are all updated with the new data""" d={} d=self.getdata(d) self.populatefs(d) self.populate(d) # Brute force - assume all entries have changed self.RefreshAllEntries() def AddEntry(self, entry): """Adds and entry into the calendar data. The entries on disk are updated by this function. @type entry: a dict containing all the fields. @param entry: an entry. It must contain a C{pos} field. You should call L{newentryfactory} to make an entry that you then modify """ self._data[entry.id]=entry self.updateonchange() def DeleteEntry(self, entry): """Deletes an entry from the calendar data. The entries on disk are updated by this function. @type entry: a dict containing all the fields. @param entry: an entry. It must contain a C{pos} field corresponding to an existing entry """ del self._data[entry.id] self.updateonchange() def DeleteEntryRepeat(self, entry, year, month, day): """Deletes a specific repeat of an entry See L{DeleteEntry}""" self._data[entry.id].suppress_repeat_entry(year, month, day) self.updateonchange() def ChangeEntry(self, oldentry, newentry): """Changes an entry in the calendar data. The entries on disk are updated by this function. """ assert oldentry.id==newentry.id self._data[newentry.id]=newentry self.updateonchange() def getentrydata(self, year, month, day): """return the entry objects for corresponding date @rtype: list""" # return data from cache if we have it res=self.entrycache.get( (year,month,day), None) if res is not None: return res # find non-repeating entries res=self.entries.get((year,month,day), []) for i in self.repeating: if i.is_active(year, month, day): res.append(i) self.entrycache[(year,month,day)] = res return res def newentryfactory(self, year, month, day): """Returns a new 'blank' entry with default fields @rtype: CalendarEntry """ # create a new entry res=CalendarEntry(year, month, day) # fill in default start & end data now=time.localtime() event_start=(year, month, day, now.tm_hour, now.tm_min) event_end=[year, month, day, now.tm_hour, now.tm_min] # we make end be the next hour, unless it has gone 11pm # in which case it is 11:59pm if event_end[3]<23: event_end[3]+=1 event_end[4]=0 else: event_end[3]=23 event_end[4]=59 res.start=event_start res.end=event_end res.description='New Event' return res def getdaybitmap(self, start, repeat): if repeat!="weekly": return 0 dayofweek=calendar.weekday(*(start[:3])) dayofweek=(dayofweek+1)%7 # normalize to sunday == 0 return [2048,1024,512,256,128,64,32][dayofweek] def OnGetEntries(self, year, month, day): """return pretty printed sorted entries for date as required by the parent L{calendarcontrol.Calendar} for display in a cell""" entry_list=self.getentrydata(year, month, day) res=[ (i.start[3], i.start[4], i.description) \ for i in entry_list if not i.allday ] res += [ (None, None, i.description) \ for i in entry_list if i.allday ] res.sort() return res def OnEdit(self, year, month, day, entry=None): """Called when the user wants to edit entries for a particular day""" if self.dialog.dirty: # user is editing a field so we don't allow edit wx.Bell() else: self.dialog.setdate(year, month, day, entry) self.dialog.Show(True) def OnTodayItem(self, evt): if evt.data: args=evt.data['datetime']+(evt.data['entry'],) self.OnEdit(*args) def OnTodayButton(self, evt): """ Called when the user goes to today cell""" super(Calendar, self).OnTodayButton(evt) if self.dialog.IsShown(): # editor dialog is up, update it self.OnEdit(*self.selecteddate) def _publish_today_events(self): now=datetime.datetime.now() l=self.getentrydata(now.year, now.month, now.day) l.sort(CalendarEntry.cmp_by_time) today_event=today.TodayCalendarEvent() for e in l: today_event.append(e.summary, { 'datetime': (now.year, now.month, now.day), 'entry': e }) today_event.broadcast() def _publish_thisweek_events(self): now=datetime.datetime.now() one_day=datetime.timedelta(1) d1=now _days=6-(now.isoweekday()%7) res=[] today_event=today.ThisWeekCalendarEvent() for i in range(_days): d1+=one_day l=self.getentrydata(d1.year, d1.month, d1.day) if l: _dow=today.dow_initials[d1.isoweekday()%7] l.sort(CalendarEntry.cmp_by_time) for i,x in enumerate(l): if i: _name=today.dow_initials[-1]+' ' else: _name=_dow+' - ' _name+=x.summary today_event.append(_name, { 'datetime': (d1.year, d1.month, d1.day), 'entry': x }) today_event.broadcast() def populate(self, dict): """Updates the internal data with the contents of C{dict['calendar']}""" if dict.get('calendar_version', None)==2: # Cal dict version 2, need to convert to current ver(3) self._data=self._convert2to3(dict.get('calendar', {}), dict.get('ringtone-index', {})) else: self._data=dict.get('calendar', {}) self.entrycache={} self.entries={} self.repeating=[] for entry in self._data: entry=self._data[entry] y,m,d,h,min=entry.start if entry.repeat is None: self.entries.setdefault((y,m,d), []).append(entry) else: self.repeating.append(entry) # tell everyone that i've changed self._publish_today_events() self._publish_thisweek_events() self.RefreshAllEntries() def populatefs(self, dict): """Saves the dict to disk""" if dict.get('calendar_version', None)==2: # Cal dict version 2, need to convert to current ver(3) cal_dict=self._convert2to3(dict.get('calendar', {}), dict.get('ringtone-index', {})) else: cal_dict=dict.get('calendar', {}) db_rr={} for k, e in cal_dict.items(): db_rr[k]=CalendarDataObject(e) database.ensurerecordtype(db_rr, calendarobjectfactory) db_rr=database.extractbitpimserials(db_rr) self.mainwindow.database.savemajordict('calendar', db_rr) return dict def getfromfs(self, dict): """Updates dict with info from disk @Note: The dictionary passed in is modified, as well as returned @rtype: dict @param dict: the dictionary to update @return: the updated dictionary""" self.thedir=self.mainwindow.calendarpath if os.path.exists(os.path.join(self.thedir, "index.idx")): # old index file exists: read, convert, and discard file dct={'result': {}} common.readversionedindexfile(os.path.join(self.thedir, "index.idx"), dct, self.versionupgrade, self.CURRENTFILEVERSION) converted=dct['result'].has_key('converted') db_r={} for k,e in dct['result'].get('calendar', {}).items(): if converted: db_r[k]=CalendarDataObject(e) else: ce=CalendarEntry() ce.set(e) db_r[k]=CalendarDataObject(ce) # save it in the new database database.ensurerecordtype(db_r, calendarobjectfactory) db_r=database.extractbitpimserials(db_r) self.mainwindow.database.savemajordict('calendar', db_r) # now that save is succesful, move file out of the way os.rename(os.path.join(self.thedir, "index.idx"), os.path.join(self.thedir, "index-is-now-in-database.bak")) # read data from the database cal_dict=self.mainwindow.database.getmajordictvalues('calendar', calendarobjectfactory) #if __debug__: # print 'Calendar.getfromfs: dicts returned from Database:' r={} for k,e in cal_dict.items(): #if __debug__: # print e ce=CalendarEntry() ce.set_db_dict(e) r[ce.id]=ce dict.update({ 'calendar': r }) return dict def versionupgrade(self, dict, version): """Upgrade old data format read from disk @param dict: The dict that was read in @param version: version number of the data on disk """ # version 0 to 1 upgrade if version==0: version=1 # they are the same # 1 to 2 if version==1: # ?d field renamed daybitmap version=2 for k in dict['result']['calendar']: entry=dict['result']['calendar'][k] entry['daybitmap']=self.getdaybitmap(entry['start'], entry['repeat']) del entry['?d'] # 2 to 3 etc if version==2: version=3 dict['result']['calendar']=self.convert_dict(dict['result'].get('calendar', {}), 2, 3) dict['result']['converted']=True # already converted # 3 to 4 etc def convert_dict(self, dict, from_version, to_version, ringtone_index={}): """ Convert the calendatr dict from one version to another. Currently only support conversion between version 2 and 3. """ if dict is None: return None if from_version==2 and to_version==3: return self._convert2to3(dict, ringtone_index) elif from_version==3 and to_version==2: return self._convert3to2(dict, ringtone_index) else: raise 'Invalid conversion' def _convert2to3(self, dict, ringtone_index): """ Convert calendar dict from version 2 to 3. """ r={} for k,e in dict.items(): ce=CalendarEntry() ce.start=e['start'] ce.end=e['end'] ce.description=e['description'] ce.alarm=e['alarm'] ce.ringtone=ringtone_index.get(e['ringtone'], {}).get('name', '') repeat=e['repeat'] if repeat is None: ce.repeat=None else: repeat_entry=RepeatEntry() if repeat=='daily': repeat_entry.repeat_type=repeat_entry.daily repeat_entry.interval=1 elif repeat=='monfri': repeat_entry.repeat_type=repeat_entry.daily repeat_entry.interval=0 elif repeat=='weekly': repeat_entry.repeat_type=repeat_entry.weekly repeat_entry.interval=1 dow=datetime.date(*e['start'][:3]).isoweekday()%7 repeat_entry.dow=1<<dow elif repeat=='monthly': repeat_entry.repeat_type=repeat_entry.monthly else: repeat_entry.repeat_type=repeat_entry.yearly s=[] for n in e.get('exceptions',[]): s.append(bptime.BPTime(n)) repeat_entry.suppressed=s ce.repeat=repeat_entry r[ce.id]=ce return r def _convert_daily_events(self, e, d): """ Conver a daily event from v3 to v2 """ rp=e.repeat if rp.interval==1: # repeat everyday d['repeat']='daily' elif rp.interval==0: # repeat every weekday d['repeat']='monfri' else: # the interval is every nth day, with n>1 # generate exceptions for those dates that are N/A d['repeat']='daily' t0=datetime.date(*e.start[:3]) t1=datetime.date(*e.end[:3]) delta_t=datetime.timedelta(1) while t0<=t1: if not e.is_active(t0.year, t0.month, t0.day): d['exceptions'].append((t0.year, t0.month, t0.day)) t0+=delta_t def _convert_weekly_events(self, e, d, idx): """ Convert a weekly event from v3 to v2 """ rp=e.repeat dow=rp.dow t0=datetime.date(*e.start[:3]) t1=t3=datetime.date(*e.end[:3]) delta_t=datetime.timedelta(1) delta_t7=datetime.timedelta(7) if (t1-t0).days>6: # end time is more than a week away t1=t0+datetime.timedelta(6) d['repeat']='weekly' res={} while t0<=t1: dow_0=t0.isoweekday()%7 if (1<<dow_0)&dow: # we have a hit, generate a weekly repeat event here dd=copy.deepcopy(d) dd['start']=(t0.year, t0.month, t0.day, e.start[3], e.start[4]) dd['daybitmap']=self.getdaybitmap(dd['start'], dd['repeat']) # generate exceptions for every nth week case t2=t0 while t2<=t3: if not e.is_active(t2.year, t2.month, t2.day): dd['exceptions'].append((t2.year, t2.month, t2.day)) t2+=delta_t7 # done, add it to the dict dd['pos']=idx res[idx]=dd idx+=1 t0+=delta_t return idx, res def _convert3to2(self, dict, ringtone_index): """Convert calendar dict from version 3 to 2.""" r={} idx=0 for k,e in dict.items(): d={} d['start']=e.start d['end']=e.end d['description']=e.description d['alarm']=e.alarm d['changeserial']=1 d['snoozedelay']=0 d['ringtone']=0 # by default try: d['ringtone']=[i for i,r in ringtone_index.items() \ if r.get('name', '')==e.ringtone][0] except: pass rp=e.repeat if rp is None: d['repeat']=None d['exceptions']=[] d['daybitmap']=0 else: s=[] for n in rp.suppressed: s.append(n.get()[:3]) d['exceptions']=s if rp.repeat_type==rp.daily: self._convert_daily_events(e, d) elif rp.repeat_type==rp.weekly: idx, rr=self._convert_weekly_events(e, d, idx) r.update(rr) continue elif rp.repeat_type==rp.monthly: d['repeat']='monthly' elif rp.repeat_type==rp.yearly: d['repeat']='yearly' d['daybitmap']=self.getdaybitmap(d['start'], d['repeat']) d['pos']=idx r[idx]=d idx+=1 if __debug__: print 'Calendar._convert3to2: V2 dict:' print r return r #------------------------------------------------------------------------------- class CalendarPrintDialog(wx.Dialog): _regular_template='cal_regular.xy' _regular_style='cal_regular_style.xy' _monthly_template='cal_monthly.xy' _monthly_style='cal_monthly_style.xy' def __init__(self, calwidget, mainwindow, config): super(CalendarPrintDialog, self).__init__(mainwindow, -1, 'Print Calendar') self._cal_widget=calwidget self._xcp=self._html=self._dns=None self._dt_index=self._dt_start=self._dt_end=None self._date_changed=self._style_changed=False self._tmp_file=common.gettempfilename("htm") # main box sizer vbs=wx.BoxSizer(wx.VERTICAL) hbs=wx.BoxSizer(wx.HORIZONTAL) # the print range box sbs=wx.StaticBoxSizer(wx.StaticBox(self, -1, 'Print Range'), wx.VERTICAL) gs=wx.FlexGridSizer(-1, 2, 5, 5) gs.AddGrowableCol(1) gs.Add(wx.StaticText(self, -1, 'Start:'), 0, wx.ALL, 0) self._start_date=wx.DatePickerCtrl(self, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY) wx.EVT_DATE_CHANGED(self, self._start_date.GetId(), self.OnDateChanged) gs.Add(self._start_date, 0, wx.ALL, 0) gs.Add(wx.StaticText(self, -1, 'End:'), 0, wx.ALL, 0) self._end_date=wx.DatePickerCtrl(self, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY) wx.EVT_DATE_CHANGED(self, self._end_date.GetId(), self.OnDateChanged) gs.Add(self._end_date, 0, wx.ALL, 0) sbs.Add(gs, 1, wx.EXPAND|wx.ALL, 5) hbs.Add(sbs, 0, wx.ALL, 5) # thye print style box self._print_style=wx.RadioBox(self, -1, 'Print Style', choices=['List View', 'Month View'], style=wx.RA_SPECIFY_ROWS) wx.EVT_RADIOBOX(self, self._print_style.GetId(), self.OnStyleChanged) hbs.Add(self._print_style, 0, wx.ALL, 5) vbs.Add(hbs, 0, wx.ALL, 5) # and the bottom buttons vbs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) hbs=wx.BoxSizer(wx.HORIZONTAL) for b in (('Print', -1, self.OnPrint), ('Page Setup', -1, self.OnPageSetup), ('Print Preview', -1, self.OnPrintPreview), ## ('Help', wx.ID_HELP, self.OnHelp), ('Close', wx.ID_CANCEL, self.OnClose)): btn=wx.Button(self, b[1], b[0]) hbs.Add(btn, 0, wx.ALIGN_CENTER|wx.ALL, 5) if b[2] is not None: wx.EVT_BUTTON(self, btn.GetId(), b[2]) # all done vbs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.EXPAND|wx.ALL, 5) self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) # constant class variables _one_day=wx.DateSpan(days=1) _empty_day=['', []] def _one_day_data(self): # generate data for 1 day r=[str(self._dt_index.GetDay())] events=[] if self._dt_start<=self._dt_index<=self._dt_end: entries=self._cal_widget.getentrydata(self._dt_index.GetYear(), self._dt_index.GetMonth()+1, self._dt_index.GetDay()) else: entries=[] self._dt_index+=self._one_day if len(entries): entries.sort(CalendarEntry.cmp_by_time) for e in entries: print_data=e.print_data events.append('%s: %s'%(print_data[0], print_data[3])) r.append(events) return r def _one_week_data(self): # generate data for 1 week dow=self._dt_index.GetWeekDay() if dow: r=[self._empty_day]*dow else: r=[] for d in range(dow, 7): r.append(self._one_day_data()) if self._dt_index.GetDay()==1: # new month break return r def _one_month_data(self): # generate data for a month m=self._dt_index.GetMonth() y=self._dt_index.GetYear() r=['%s %d'%(self._dt_index.GetMonthName(m), y)] while self._dt_index.GetMonth()==m: r.append(self._one_week_data()) return r def _get_monthly_data(self): """ generate a dict suitable to print monthly events """ res=[] self._dt_index=wx.DateTimeFromDMY(1, self._dt_start.GetMonth(), self._dt_start.GetYear()) while self._dt_index<=self._dt_end: res.append(self._one_month_data()) return res def _get_list_data(self): """ generate a dict suitable for printing""" self._dt_index=wx.DateTimeFromDMY(self._dt_start.GetDay(), self._dt_start.GetMonth(), self._dt_start.GetYear()) current_month=None res=a_month=month_events=[] while self._dt_index<=self._dt_end: y=self._dt_index.GetYear() m=self._dt_index.GetMonth() d=self._dt_index.GetDay() entries=self._cal_widget.getentrydata(y, m+1, d) self._dt_index+=self._one_day if not len(entries): # no events on this day continue entries.sort(CalendarEntry.cmp_by_time) if m!=current_month: # save the current month if len(month_events): a_month.append(month_events) res.append(a_month) # start a new month current_month=m a_month=['%s %d'%(self._dt_index.GetMonthName(m), y)] month_events=[] # go through the entries and build a list of print data for i,e in enumerate(entries): if i: date_str=day_str='' else: date_str=str(d) day_str=self._dt_index.GetWeekDayName( self._dt_index.GetWeekDay()-1, wx.DateTime.Name_Abbr) month_events.append([date_str, day_str]+e.print_data) if len(month_events): # data left in the list a_month.append(month_events) res.append(a_month) return res def _gen_print_data(self): if not self._date_changed and \ not self._style_changed and \ self._html is not None: # already generate the print data, no changes needed return self._dt_start=self._start_date.GetValue() self._dt_end=self._end_date.GetValue() if not self._dt_start.IsValid() or not self._dt_end.IsValid(): # invalid print range return print_data=( (self._regular_template, self._regular_style, self._get_list_data), (self._monthly_template, self._monthly_style, self._get_monthly_data)) print_style=self._print_style.GetSelection() # tell the calendar widget to give me the dict I need print_dict=print_data[print_style][2]() # generate the html data if self._xcp is None: # build the whole document template self._xcp=xyaptu.xcopier(None) tmpl=file(guihelper.getresourcefile(print_data[print_style][0]), 'rt').read() self._xcp.setupxcopy(tmpl) elif self._style_changed: # just update the template tmpl=file(guihelper.getresourcefile(print_data[print_style][0]), 'rt').read() self._xcp.setupxcopy(tmpl) if self._dns is None: self._dns={ 'common': __import__('common') } self._dns['guihelper']=__import__('guihelper') self._dns['events']=[] self._dns['events']=print_dict self._dns['date_range']='%s - %s'%\ (self._dt_start.FormatDate(), self._dt_end.FormatDate()) html=self._xcp.xcopywithdns(self._dns.copy()) # apply styles sd={'styles': {}, '__builtins__': __builtins__ } try: execfile(guihelper.getresourcefile(print_data[print_style][1]), sd, sd) except UnicodeError: common.unicode_execfile(guihelper.getresourcefile(print_data[print_style][1]), sd, sd) try: self._html=bphtml.applyhtmlstyles(html, sd['styles']) except: if __debug__: file('debug.html', 'wt').write(html) raise self._date_changed=self._style_change=False def OnDateChanged(self, _): self._date_changed=True def OnStyleChanged(self, _): self._style_changed=True def OnPrint(self, _): self._gen_print_data() wx.GetApp().htmlprinter.PrintText(self._html) def OnPageSetup(self, _): wx.GetApp().htmlprinter.PageSetup() def OnPrintPreview(self, _): self._gen_print_data() wx.GetApp().htmlprinter.PreviewText(self._html) ## file(self._tmp_file, 'wt').write(self._html) ## webbrowser.open('file://localhost/'+self._tmp_file) def OnHelp(self, _): pass def OnClose(self, _): try: # remove the temp file, ignore exception if file does not exist os.remove(self._tmp_file) except: pass self.EndModal(wx.ID_CANCEL)
bpcalendar.py.diff
(application/octet-stream, 854 B)
Index: bpcalendar.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/bpcalendar.py,v
retrieving revision 1.31
diff -u -r1.31 bpcalendar.py
--- bpcalendar.py 15 Aug 2005 22:54:35 -0000 1.31
+++ bpcalendar.py 8 Oct 2005 18:17:10 -0000
@@ -1382,7 +1382,10 @@
html=self._xcp.xcopywithdns(self._dns.copy())
# apply styles
sd={'styles': {}, '__builtins__': __builtins__ }
- execfile(guihelper.getresourcefile(print_data[print_style][1]), sd, sd)
+ try:
+ execfile(guihelper.getresourcefile(print_data[print_style][1]), sd, sd)
+ except UnicodeError:
+ common.unicode_execfile(guihelper.getresourcefile(print_data[print_style][1]), sd, sd)
try:
self._html=bphtml.applyhtmlstyles(html, sd['styles'])
except:
com_brew.py
(text/plain, 33.3 KB)
### BITPIM ### ### Copyright (C) 2003-2004 Roger Binns <[email protected]> ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the BitPim license as detailed in the LICENSE file. ### ### $Id: com_brew.py,v 1.49 2005/10/07 00:06:30 sawecw Exp $ """Implements the "Brew" filesystem protocol""" import os import p_brew import time import cStringIO import com_phone import prototypes import common class BrewNotSupported(Exception): """This phone not supported""" pass class BrewCommandException(Exception): def __init__(self, errnum, str=None): if str is None: str="Brew Error 0x%02x" % (errnum,) Exception.__init__(self, str) self.errnum=errnum class BrewNoMoreEntriesException(BrewCommandException): def __init__(self, errnum=0x1c): BrewCommandException.__init__(self, errnum, "No more directory entries") class BrewNoSuchDirectoryException(BrewCommandException): def __init__(self, errnum=0x08): BrewCommandException.__init__(self, errnum, "No such directory") class BrewNoSuchFileException(BrewCommandException): def __init__(self, errnum=0x06): BrewCommandException.__init__(self, errnum, "No such file") class BrewBadPathnameException(BrewCommandException): def __init__(self, errnum=0x1a): BrewCommandException.__init__(self, errnum, "Bad pathname") class BrewFileLockedException(BrewCommandException): def __init__(self, errnum=0x0b): BrewCommandException.__init__(self, errnum, "File is locked") class BrewNameTooLongException(BrewCommandException): def __init__(self, errnum=0x0d): BrewCommandException.__init__(self, errnum, "Name is too long") class BrewDirectoryExistsException(BrewCommandException): def __init__(self, errnum=0x07): BrewCommandException.__init__(self, errnum, "Directory already exists") modeignoreerrortypes=com_phone.modeignoreerrortypes+(BrewCommandException,common.CommsDataCorruption) class DebugBrewProtocol: """ Emulate a phone file system using a local file system. This is used when you may not have access to a physical phone, but have a copy of its file system. """ _fs_path='' def __init__(self): pass def getfirmwareinformation(self): self.log("Getting firmware information") def explore0c(self): self.log("Trying stuff with command 0x0c") def offlinerequest(self, reset=False): self.log("Taking phone offline") if reset: self.log("Resetting phone") def modemmoderequest(self): self.log("Attempting to put phone in modem mode") def mkdir(self, name): self.log("Making directory '"+name+"'") os.mkdir(os.path.join(self._fs_path, name)) def mkdirs(self, directory): if len(directory)<1: return dirs=directory.split('/') for i in range(0,len(dirs)): try: self.mkdir("/".join(dirs[:i+1])) # basically mkdir -p except: pass def rmdir(self,name): self.log("Deleting directory '"+name+"'") os.rmdir(os.path.join(self._fs_path, name)) def rmfile(self,name): self.log("Deleting file '"+name+"'") os.remove(os.path.join(self._fs_path, name)) def rmdirs(self, path): self.progress(0,1, "Listing child files and directories") all=self.getfilesystem(path, 100) keys=all.keys() keys.sort() keys.reverse() count=0 for k in keys: self.progress(count, len(keys), "Deleting "+k) count+=1 if all[k]['type']=='directory': self.rmdir(k) else: self.rmfile(k) self.rmdir(path) def getfilesystem(self, dir="", recurse=0): results={} self.log("Listing dir '"+dir+"'") _pwd=os.path.join(self._fs_path, dir) for _root,_dir,_file in os.walk(_pwd): break for f in _file: _stat=os.stat(os.path.join(_pwd, f)) _date=_stat[8] _name=dir+'/'+f results[_name]={ 'name': _name, 'type': 'file', 'size': _stat[6], 'date': (_date, time.strftime("%x %X", time.gmtime(_date))) } for d in _dir: results[d]={ 'name': d, 'type': 'directory' } if recurse>0: results.update(self.getfilesystem(os.path.join(dir, d), recurse-1)) return results def statfile(self, name): _stat=os.stat(os.path.join(self._fs_path, name)) _date=_stat[8] results={ 'name': name, 'type': 'file', 'size': _stat[6], 'date': (_date, time.strftime("%x %X", time.gmtime(_date))) } return results def writefile(self, name, contents): self.log("Writing file '"+name+"' bytes "+`len(contents)`) file(os.path.join(self._fs_path, name), 'wb').write(contents) def getfilecontents(self, name, use_cache=False): self.log("Getting file contents '"+name+"'") try: return file(os.path.join(self._fs_path, name), 'rb').read() except: raise BrewNoSuchFileException def _setmodebrew(self): self.log('_setmodebrew: in mode BREW') return True def sendbrewcommand(self, request, responseclass, callsetmode=True): raise NotImplemetedError def log(self, s): print s def logdata(self, s, data, klass=None): print s class RealBrewProtocol: "Talk to a phone using the 'brew' protocol" MODEBREW="modebrew" brewterminator="\x7e" # phone uses Jan 1, 1980 as epoch. Python uses Jan 1, 1970. This is difference # plus a fudge factor of 4 days, 17 hours for no reason I can find _brewepochtounix=315532800+406800 def __init__(self): pass def getfirmwareinformation(self): self.log("Getting firmware information") req=p_brew.firmwarerequest() res=self.sendbrewcommand(req, p_brew.firmwareresponse) def explore0c(self): self.log("Trying stuff with command 0x0c") req=p_brew.testing0crequest() res=self.sendbrewcommand(req, p_brew.testing0cresponse) def offlinerequest(self, reset=False): req=p_brew.setmoderequest() req.request=1 self.log("Taking phone offline") self.sendbrewcommand(req, p_brew.setmoderesponse) if reset: req=p_brew.setmoderequest() req.request=2 self.log("Resetting phone") self.sendbrewcommand(req, p_brew.setmoderesponse) def modemmoderequest(self): # Perhaps we should modify sendbrewcommand to have an option to # not be picky about response. self.log("Attempting to put phone in modem mode") req=p_brew.setmodemmoderequest() buffer=prototypes.buffer() req.writetobuffer(buffer) data=buffer.getvalue() self.logdata("brew request", data, req) data=common.pppescape(data+common.crcs(data))+common.pppterminator self.comm.write(data) # Response could be text or a packet self.comm.readsome(numchars=5) self.mode=self.MODENONE # Probably should add a modem mode def mkdir(self, name): self.log("Making directory '"+name+"'") req=p_brew.mkdirrequest() req.dirname=name self.sendbrewcommand(req, p_brew.mkdirresponse) def mkdirs(self, directory): if len(directory)<1: return dirs=directory.split('/') for i in range(0,len(dirs)): try: self.mkdir("/".join(dirs[:i+1])) # basically mkdir -p except: pass def rmdir(self,name): self.log("Deleting directory '"+name+"'") req=p_brew.rmdirrequest() req.dirname=name self.sendbrewcommand(req, p_brew.rmdirresponse) def rmfile(self,name): self.log("Deleting file '"+name+"'") req=p_brew.rmfilerequest() req.filename=name self.sendbrewcommand(req, p_brew.rmfileresponse) file_cache.clear(name) def rmdirs(self, path): self.progress(0,1, "Listing child files and directories") all=self.getfilesystem(path, 100) keys=all.keys() keys.sort() keys.reverse() count=0 for k in keys: self.progress(count, len(keys), "Deleting "+k) count+=1 if all[k]['type']=='directory': self.rmdir(k) else: self.rmfile(k) self.rmdir(path) def listsubdirs(self, dir='', recurse=0): results={} self.log("Listing subdirs in dir: '"+dir+"'") req=p_brew.listdirectoryrequest() req.dirname=dir for i in xrange(10000): try: req.entrynumber=i res=self.sendbrewcommand(req,p_brew.listdirectoryresponse) # sometimes subdir can already include the parent directory f=res.subdir.rfind("/") if f>=0: subdir=res.subdir[f+1:] else: subdir=res.subdir if len(dir): subdir=dir+"/"+subdir results[subdir]={ 'name': subdir, 'type': 'directory' } except BrewNoMoreEntriesException: break if recurse: for k,_subdir in results.items(): results.update(self.listsubdirs(_subdir['name'], recurse-1)) return results def listfiles(self, dir=''): results={} self.log("Listing files in dir: '"+dir+"'") req=p_brew.listfilerequest() req.dirname=dir # self.log("file listing 0x0b command") for i in xrange(10000): try: req.entrynumber=i res=self.sendbrewcommand(req,p_brew.listfileresponse) results[res.filename]={ 'name': res.filename, 'type': 'file', 'size': res.size } if res.date==0: results[res.filename]['date']=(0, "") else: try: date=res.date+self._brewepochtounix results[res.filename]['date']=(date, time.strftime("%x %X", time.gmtime(date))) except: # invalid date - see SF bug #833517 results[res.filename]['date']=(0, "") except BrewNoMoreEntriesException: break return results def getfilesystem(self, dir="", recurse=0): self.log("Getting file system in dir '"+dir+"'") results=self.listsubdirs(dir) subdir_list=[x['name'] for k,x in results.items()] results.update(self.listfiles(dir)) if recurse: for _subdir in subdir_list: results.update(self.getfilesystem(_subdir, recurse-1)) return results def statfile(self, name): # return the status of the file try: self.log('stat file '+name) req=p_brew.statfilerequest() req.filename=name res=self.sendbrewcommand(req, p_brew.statfileresponse) results={ 'name': name, 'type': 'file', 'size': res.size } if res.date==0: results['date']=(0, '') else: try: date=res.date+self._brewepochtounix results['date']=(date, time.strftime("%x %X", time.gmtime(date))) except: # invalid date - see SF bug #833517 results['date']=(0, '') return results except: # something happened, we don't have any info on this file if __debug__: raise return None def writefile(self, name, contents): start=time.time() self.log("Writing file '"+name+"' bytes "+`len(contents)`) desc="Writing "+name req=p_brew.writefilerequest() req.filesize=len(contents) req.data=contents[:0x100] req.filename=name self.sendbrewcommand(req, p_brew.writefileresponse) # do remaining blocks numblocks=len(contents)/0x100 count=0 for offset in range(0x100, len(contents), 0x100): req=p_brew.writefileblockrequest() count+=1 if count>=0x100: count=1 if count % 5==0: self.progress(offset>>8,numblocks,desc) req.blockcounter=count req.thereismore=offset+0x100<len(contents) block=contents[offset:] l=min(len(block), 0x100) block=block[:l] req.data=block self.sendbrewcommand(req, p_brew.writefileblockresponse) end=time.time() if end-start>3: self.log("Wrote "+`len(contents)`+" bytes at "+`int(len(contents)/(end-start))`+" bytes/second") def getfilecontents(self, file, use_cache=False): if use_cache: node=self.statfile(file) if node and file_cache.hit(file, node['date'][0], node['size']): self.log('Reading from cache: '+file) _data=file_cache.data(file) if _data: return _data self.log('Cache file corrupted and discarded') start=time.time() self.log("Getting file contents '"+file+"'") desc="Reading "+file data=cStringIO.StringIO() req=p_brew.readfilerequest() req.filename=file res=self.sendbrewcommand(req, p_brew.readfileresponse) filesize=res.filesize data.write(res.data) counter=0 while res.thereismore: counter+=1 if counter>0xff: counter=0x01 if counter%5==0: self.progress(data.tell(), filesize, desc) req=p_brew.readfileblockrequest() req.blockcounter=counter res=self.sendbrewcommand(req, p_brew.readfileblockresponse) data.write(res.data) self.progress(1,1,desc) data=data.getvalue() # give the download speed if we got a non-trivial amount of data end=time.time() if end-start>3: self.log("Read "+`filesize`+" bytes at "+`int(filesize/(end-start))`+" bytes/second") if filesize!=len(data): self.log("expected size "+`filesize`+" actual "+`len(data)`) self.raisecommsexception("Brew file read is incorrect size", common.CommsDataCorruption) if use_cache and node: file_cache.add(file, node.get('date', [0])[0], data) return data class DirCache: """This is a class that lets you do various filesystem manipulations and it remembers the data. Typical usage would be if you make changes to files (adding, removing, rewriting) and then have to keep checking if files exist, add sizes etc. This class saves the hassle of rereading the directory every single time. Note that it will only see changes you make via this class. If you go directly to the Brew class then those won't be seen. """ def __init__(self, target): "@param target: where operations should be done after recording them here" self.__target=target self.__cache={} def rmfile(self, filename): res=self.__target.rmfile(filename) node=self._getdirectory(brewdirname(filename)) if node is None: # we didn't have it return del node[brewbasename(filename)] return res def stat(self, filename): node=self._getdirectory(brewdirname(filename), ensure=True) return node.get(brewbasename(filename), None) def readfile(self, filename): node=self._getdirectory(brewdirname(filename), ensure=True) file=node.get(brewbasename(filename), None) if file is None: raise BrewNoSuchFileException() # This class only populates the 'data' portion of the file obj when needed data=file.get('data', None) if data is None: data=self.__target.getfilecontents(filename) file['data']=data return data def writefile(self, filename, contents): res=self.__target.writefile(filename, contents) node=self._getdirectory(brewdirname(filename), ensure=True) # we can't put the right date in since we have no idea # what the timezone (or the time for that matter) on the # phone is stat=node.get(brewbasename(filename), {'name': filename, 'type': 'file', 'date': (0, "")}) stat['size']=len(contents) stat['data']=contents node[brewbasename(filename)]=stat return res def _getdirectory(self, dirname, ensure=False): if not ensure: return self.__cache.get(dirname, None) node=self.__cache.get(dirname, None) if node is not None: return node node={} fs=self.__target.getfilesystem(dirname) for filename in fs.keys(): node[brewbasename(filename)]=fs[filename] self.__cache[dirname]=node return node def _setmodebrew(self): req=p_brew.memoryconfigrequest() respc=p_brew.memoryconfigresponse for baud in 0, 38400,115200: if baud: if not self.comm.setbaudrate(baud): continue try: self.sendbrewcommand(req, respc, callsetmode=False) return True except modeignoreerrortypes: pass # send AT$CDMG at various speeds for baud in (0, 115200, 19200, 230400): if baud: if not self.comm.setbaudrate(baud): continue print "Baud="+`baud` try: for line in self.comm.sendatcommand("+GMM"): if line.find("SPH-A700")>0: raise BrewNotSupported("This phone is not supported by BitPim", self.desc) except modeignoreerrortypes: self.log("No response to AT+GMM") except: print "GMM Exception" self.mode=self.MODENONE self.comm.shouldloop=True raise try: self.comm.write("AT$QCDMG\r\n") except: # some issue during writing such as user pulling cable out self.mode=self.MODENONE self.comm.shouldloop=True raise try: # if we got OK back then it was success if self.comm.readsome().find("OK")>=0: break except modeignoreerrortypes: self.log("No response to setting QCDMG mode") # verify if we are in DM mode for baud in 0,38400,115200: if baud: if not self.comm.setbaudrate(baud): continue try: self.sendbrewcommand(req, respc, callsetmode=False) return True except modeignoreerrortypes: pass return False def sendbrewcommand(self, request, responseclass, callsetmode=True): if callsetmode: self.setmode(self.MODEBREW) buffer=prototypes.buffer() request.writetobuffer(buffer) data=buffer.getvalue() self.logdata("brew request", data, request) data=common.pppescape(data+common.crcs(data))+common.pppterminator firsttwo=data[:2] try: # we logged above, and below data=self.comm.writethenreaduntil(data, False, common.pppterminator, logreaduntilsuccess=False) except modeignoreerrortypes: self.mode=self.MODENONE self.raisecommsdnaexception("manipulating the filesystem") self.comm.success=True origdata=data # sometimes there is junk at the begining, eg if the user # turned off the phone and back on again. So if there is more # than one 7e in the escaped data we should start after the # second to last one d=data.rfind(common.pppterminator,0,-1) if d>=0: self.log("Multiple packets in data - taking last one starting at "+`d+1`) self.logdata("Original data", origdata, None) data=data[d+1:] # turn it back to normal data=common.pppunescape(data) # sometimes there is other crap at the begining d=data.find(firsttwo) if d>0: self.log("Junk at begining of packet, data at "+`d`) self.logdata("Original data", origdata, None) self.logdata("Working on data", data, None) data=data[d:] # take off crc and terminator crc=data[-3:-1] data=data[:-3] calccrc=common.crcs(data) if calccrc!=crc: self.logdata("Original data", origdata, None) self.logdata("Working on data", data, None) raise common.CommsDataCorruption("Brew packet failed CRC check", self.desc) # log it self.logdata("brew response", data, responseclass) if firsttwo=="Y\x0c" and data==firsttwo: # we are getting an echo - the modem port has been selected # instead of diagnostics port raise common.CommsWrongPort("The port you are using is echoing data back, and is not valid for Brew data. Most likely you have selected the modem interface when you should be using the diagnostic interface.", self.desc) # look for errors if data[0]=="Y" and data[2]!="\x00": # Y is 0x59 which is brew command prefix err=ord(data[2]) if err==0x1c: raise BrewNoMoreEntriesException() if err==0x08: raise BrewNoSuchDirectoryException() if err==0x06: raise BrewNoSuchFileException() if err==0x1a: raise BrewBadPathnameException() if err==0x0b: raise BrewFileLockedException() if err==0x0d: raise BrewNameTooLongException() if err==0x07: raise BrewDirectoryExistsException() raise BrewCommandException(err) # parse data buffer=prototypes.buffer(data) res=responseclass() try: res.readfrombuffer(buffer) except: # we had an exception so log the data even if protocol log # view is not available self.log(formatpacketerrorlog("Error decoding response", origdata, data, responseclass)) raise return res class BrewProtocol(RealBrewProtocol): """This is just a wrapper class that allows the manipulation between RealBrewProtocol and DebugBrewProtocol classes. """ def __init__(self): # if the env var PHONE_FS is set, we're debugging! phone_path=os.environ.get('PHONE_FS', None) if __debug__ and phone_path: print 'Debug Phone File System:',phone_path # we probably need to do this only once for the whole class, # but what the heck! DebugBrewProtocol._fs_path=os.path.normpath(phone_path) self._update_base_class(self.__class__) def _update_base_class(self, klass): # update the RealBrewProtocol class to DebugBrewProtocol one. _bases=[] found=False for e in klass.__bases__: if e==RealBrewProtocol: _bases.append(DebugBrewProtocol) found=True else: _bases.append(e) if found: klass.__bases__=tuple(_bases) else: for e in _bases: self._update_base_class(e) def formatpacketerrorlog(str, origdata, data, klass): # copied from guiwidgets.LogWindow.logdata hd="" if data is not None: hd="Data - "+`len(data)`+" bytes\n" if klass is not None: try: hd+="<#! %s.%s !#>\n" % (klass.__module__, klass.__name__) except: klass=klass.__class__ hd+="<#! %s.%s !#>\n" % (klass.__module__, klass.__name__) hd+=common.datatohexstring(data) if origdata is not None: hd+="\nOriginal Data - "+`len(data)`+" bytes\n"+common.datatohexstring(origdata) return str+" "+hd def brewbasename(str): "returns basename of str" if str.rfind("/")>0: return str[str.rfind("/")+1:] return str def brewdirname(str): "returns dirname of str" if str.rfind("/")>0: return str[:str.rfind("/")] return str class SPURIOUSZERO(prototypes.BaseProtogenClass): """This is a special class used to consume the spurious zero in some p_brew.listfileresponse The three bytes are formatted as follows: - An optional 'null' byte (this class) - A byte specifying how long the directory name portion is, including trailing slash - A byte specifying the length of the whole name - The bytes of the filename (which includes the full directory name) Fun and games ensue because files in the root directory have a zero length directory name, so we have some heuristics to try and distinguish if the first byte is the spurious zero or not Also allow for zero length filenames. """ def __init__(self, *args, **kwargs): super(SPURIOUSZERO,self).__init__(*args, **kwargs) self._value=None if self._ismostderived(SPURIOUSZERO): self._update(args, kwargs) def _update(self, args, kwargs): super(SPURIOUSZERO, self)._update(args, kwargs) self._complainaboutunusedargs(SPURIOUSZERO, kwargs) if len(args): raise TypeError("Unexpected arguments "+`args`) def readfrombuffer(self, buf): self._bufferstartoffset=buf.getcurrentoffset() # there are several cases this code has to deal with # # The data is ordered like this: # # optional spurious zero (sz) # dirlen # fulllen # name # # These are the various possibilities. The first two # are a file in the root directory (dirlen=0), with the other # two being a file in a subdirectory (dirlen>0). fulllen # is always >0 # # A: dirlen=0 fulllen name # B: sz dirlen=0 fulllen name # C: dirlen>0 fulllen name # D: sz dirlen>0 fulllen name while True: # this is just used so we can break easily # CASE C if buf.peeknextbyte()!=0: self._value=-1 break # CASE B if buf.peeknextbyte(1)==0: # If the filename is empty, we should see two zeros if buf.howmuchmore()==2: break self._value=buf.getnextbyte() # consume sz break # A & D are harder to distinguish since they both consist of a zero # followed by non-zero. Consequently we examine the data for # consistency all=buf.peeknextbytes(min(max(2+buf.peeknextbyte(1), 3+buf.peeknextbyte(2)), buf.howmuchmore())) # are the values consistent for D? ddirlen=ord(all[1]) dfulllen=ord(all[2]) if ddirlen<dfulllen and ddirlen<len(all)-3 and all[3+ddirlen-1]=='/': self._value=buf.getnextbyte() # consume sz break # case C, do nothing self._value=-2 break self._bufferendoffset=buf.getcurrentoffset() def writetobuffer(self, buf): raise NotImplementedError() def packetsize(self): raise NotImplementedError() def getvalue(self): "Returns the string we are" if self._value is None: raise prototypes.ValueNotSetException() return self._value file_cache=None class EmptyFileCache(object): def __init__(self, bitpim_path): self._path=None self._cache_file_name=None self._data={ 'file_index': 0 } self.esn=None def hit(self, file_name, datetime, data_len): return False def data(self, file_name): return None def add(self, file_name, datetime, data): pass def clear(self, file_name): pass def set_path(self, bitpim_path): try: print 'setting path to',`bitpim_path` if not bitpim_path: raise ValueError # set the paths self.__class__=FileCache self._path=os.path.join(bitpim_path, 'cache') self._cache_file_name=os.path.join(self._path, self._cache_index_file_name) self._check_path() self._read_index() self._write_index() except: self.__class__=EmptyFileCache class FileCache(object): _cache_index_file_name='index.idx' current_version=1 def __init__(self, bitpim_path): self._path=os.path.join(bitpim_path, 'cache') self._cache_file_name=os.path.join(self._path, self._cache_index_file_name) self._data={ 'file_index': 0 } self.esn=None try: if not bitpim_path: raise ValueError self._check_path() self._read_index() self._write_index() except: # something's wrong, disable caching self.__class__=EmptyFileCache def _check_path(self): try: os.makedirs(self._path) except: pass if not os.path.isdir(self._path): raise Exception("Bad cache directory: '"+self._path+"'") def _read_index(self): self._check_path() d={ 'result': {} } try: common.readversionedindexfile(self._cache_file_name, d, None, self.current_version) self._data.update(d['result']) except: print 'failed to read cache index file' def _write_index(self): self._check_path() common.writeversionindexfile(self._cache_file_name, self._data, self.current_version) def _entry(self, file_name): k=self._data.get(self.esn, None) if k: return k.get(file_name, None) def hit(self, file_name, datetime, data_len): try: e=self._entry(file_name) if e: return e['datetime'] and e['datetime']==datetime and \ e['size']==data_len return False except: if __debug__: raise return False def data(self, file_name): try: e=self._entry(file_name) if e: _data=file(os.path.join(self._path, e['cache']), 'rb').read() if len(_data)==e['size']: return _data except IOError: return None except: if __debug__: raise return None def add(self, file_name, datetime, data): try: if self.esn: e=self._entry(file_name) if not e: # entry does not exist, create a new one self._data.setdefault(self.esn, {})[file_name]={} e=self._data[self.esn][file_name] e['cache']='F%05d'%self._data['file_index'] self._data['file_index']+=1 # entry exists, just update the data e['datetime']=datetime e['size']=len(data) _cache_file_name=os.path.join(self._path, e['cache']) try: file(_cache_file_name, 'wb').write(data) self._write_index() except IOError: # failed to write to cache file, drop this entry self._read_index() except: if __debug__: raise def clear(self, file_name): try: # clear this entry if it exists e=self._entry(file_name) if e: try: # remove the cache file os.remove(os.path.join(self._path, e['cache'])) except: pass # and remove the entry del self._data[self.esn][file_name] self._write_index() except: if __debug__: raise def set_path(self, bitpim_path): try: print 'setting path to',`bitpim_path` if not bitpim_path: raise ValueError # set the paths self.__class__=FileCache self._path=os.path.join(bitpim_path, 'cache') self._cache_file_name=os.path.join(self._path, self._cache_index_file_name) self._check_path() self._read_index() self._write_index() except: raise self.__class__=EmptyFileCache
com_brew.py.diff
(application/octet-stream, 827 B)
Index: com_brew.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/com_brew.py,v
retrieving revision 1.49
diff -u -r1.49 com_brew.py
--- com_brew.py 7 Oct 2005 00:06:30 -0000 1.49
+++ com_brew.py 8 Oct 2005 18:27:15 -0000
@@ -808,7 +808,7 @@
pass
def set_path(self, bitpim_path):
try:
- print 'setting path to',bitpim_path
+ print 'setting path to',`bitpim_path`
if not bitpim_path:
raise ValueError
# set the paths
@@ -938,7 +938,7 @@
def set_path(self, bitpim_path):
try:
- print 'setting path to',bitpim_path
+ print 'setting path to',`bitpim_path`
if not bitpim_path:
raise ValueError
# set the paths
database.py
(text/plain, 38.7 KB)
### BITPIM ### ### Copyright (C) 2004 Roger Binns <[email protected]> ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the BitPim license as detailed in the LICENSE file. ### ### $Id: database.py,v 1.26 2005/07/12 21:23:57 djpham Exp $ """Interface to the database""" import os import copy import time import sha import random import apsw import common ### ### The first section of this file deals with typical objects used to ### represent data items and various methods for wrapping them. ### class basedataobject(dict): """A base object derived from dict that is used for various records. Existing code can just continue to treat it as a dict. New code can treat it as dict, as well as access via attribute names (ie object["foo"] or object.foo). attribute name access will always give a result includes None if the name is not in the dict. As a bonus this class includes checking of attribute names and types in non-production runs. That will help catch typos etc. For production runs we may be receiving data that was written out by a newer version of BitPim so we don't check or error.""" # which properties we know about _knownproperties=[] # which ones we know about that should be a list of dicts _knownlistproperties={'serials': ['sourcetype', '*']} # which ones we know about that should be a dict _knowndictproperties={} if __debug__: # in debug code we check key name and value types def _check_property(self,name,value=None): # check it assert isinstance(name, (str, unicode)), "keys must be a string type" assert name in self._knownproperties or name in self._knownlistproperties or name in self._knowndictproperties, "unknown property named '"+name+"'" if value is None: return if name in getattr(self, "_knownlistproperties"): assert isinstance(value, list), "list properties ("+name+") must be given a list as value" # each list member must be a dict for v in value: self._check_property_dictvalue(name,v) return if name in getattr(self, "_knowndictproperties"): assert isinstance(value, dict), "dict properties ("+name+") must be given a dict as value" self._check_property_dictvalue(name,value) return # the value must be a basetype supported by apsw/SQLite assert isinstance(value, (str, unicode, buffer, int, long, float)), "only serializable types supported for values" def _check_property_dictvalue(self, name, value): assert isinstance(value, dict), "item(s) in "+name+" (a list) must be dicts" assert name in self._knownlistproperties or name in self._knowndictproperties if name in self._knownlistproperties: for key in value: assert key in self._knownlistproperties[name] or '*' in self._knownlistproperties[name], "dict key "+key+" as member of item in list "+name+" is not known" v=value[key] assert isinstance(v, (str, unicode, buffer, int, long, float)), "only serializable types supported for values" elif name in self._knowndictproperties: for key in value: assert key in self._knowndictproperties[name] or '*' in self._knowndictproperties[name], "dict key "+key+" as member of dict in item "+name+" is not known" v=value[key] assert isinstance(v, (str, unicode, buffer, int, long, float)), "only serializable types supported for values" def update(self, items): assert isinstance(items, dict), "update only supports dicts" # Feel free to fix this code ... for k in items: self._check_property(k, items[k]) super(basedataobject, self).update(items) def __getitem__(self, name): # check when they are retrieved, not set. I did try # catching the append method, but the layers of nested # namespaces got too confused self._check_property(name) v=super(basedataobject, self).__getitem__(name) self._check_property(name, v) return v def __setitem__(self, name, value): self._check_property(name, value) super(basedataobject,self).__setitem__(name, value) def __setattr__(self, name, value): # note that we map setattr to update the dict self._check_property(name, value) self.__setitem__(name, value) def __getattr__(self, name): if name not in self._knownproperties and name not in self._knownlistproperties and name not in self._knowndictproperties: raise AttributeError(name) self._check_property(name) if name in self.keys(): return self[name] return None def __delattr__(self, name): self._check_property(name) if name in self.keys(): del self[name] else: # non-debug mode - we don't do any attribute name/value type # checking as the data may (legitimately) be from a newer # version of the program. def __setattr__(self, name, value): # note that we map setattr to update the dict super(basedataobject,self).__setitem__(name, value) def __getattr__(self, name): # and getattr checks the dict if name not in self._knownproperties and name not in self._knownlistproperties and name not in self._knowndictproperties: raise AttributeError(name) if name in self.keys(): return self[name] return None def __delattr__(self, name): if name in self.keys(): del self[name] # various methods for manging serials def GetBitPimSerial(self): "Returns the BitPim serial for this item" if "serials" not in self: raise KeyError("no bitpim serial present") for v in self.serials: if v["sourcetype"]=="bitpim": return v["id"] raise KeyError("no bitpim serial present") # rng seeded at startup _persistrandom=random.Random() _shathingy=None def _getnextrandomid(self, item): """Returns random ids used to give unique serial numbers to items @param item: any object - its memory location is used to help randomness @returns: a 20 character hexdigit string """ if basedataobject._shathingy is None: basedataobject._shathingy=sha.new() basedataobject._shathingy.update(`basedataobject._persistrandom.random()`) basedataobject._shathingy.update(`id(self)`) basedataobject._shathingy.update(`basedataobject._persistrandom.random()`) basedataobject._shathingy.update(`id(item)`) return basedataobject._shathingy.hexdigest() def EnsureBitPimSerial(self): "Ensures this entry has a serial" if self.serials is None: self.serials=[] for v in self.serials: if v["sourcetype"]=="bitpim": return self.serials.append({'sourcetype': "bitpim", "id": self._getnextrandomid(self.serials)}) class dataobjectfactory: "Called by the code to read in objects when it needs a new object container" def __init__(self, dataobjectclass=basedataobject): self.dataobjectclass=dataobjectclass if __debug__: def newdataobject(self, values={}): v=self.dataobjectclass() if len(values): v.update(values) return v else: def newdataobject(self, values={}): return self.dataobjectclass(values) def extractbitpimserials(dict): """Returns a new dict with keys being the bitpim serial for each row. Each item must be derived from basedataobject""" res={} for record in dict.itervalues(): res[record.GetBitPimSerial()]=record return res def ensurebitpimserials(dict): """Ensures that all records have a BitPim serial. Each item must be derived from basedataobject""" for record in dict.itervalues(): record.EnsureBitPimSerial() def findentrywithbitpimserial(dict, serial): """Returns the entry from dict whose bitpim serial matches serial""" for record in dict.itervalues(): if record.GetBitPimSerial()==serial: return record raise KeyError("not item with serial "+serial+" found") def ensurerecordtype(dict, factory): for key,record in dict.iteritems(): if not isinstance(record, basedataobject): dict[key]=factory.newdataobject(record) # a factory that uses dicts to allocate new data objects dictdataobjectfactory=dataobjectfactory(dict) ### ### Actual database interaction is from this point on ### if __debug__: # Change this to True to see what is going on under the hood. It # will produce a lot of output! TRACE=False else: TRACE=False def ExclusiveWrapper(method): """Wrap a method so that it has an exclusive lock on the database (noone else can read or write) until it has finished""" # note that the existing threading safety checks in apsw will # catch any thread abuse issues. def _transactionwrapper(*args, **kwargs): # nb self is the Database instance self=args[0] self.excounter+=1 self.transactionwrite=False if self.excounter==1: print "BEGIN EXCLUSIVE TRANSACTION" self.cursor.execute("BEGIN EXCLUSIVE TRANSACTION") self._schemacache={} try: try: success=True return method(*args, **kwargs) except: success=False raise finally: self.excounter-=1 if self.excounter==0: w=self.transactionwrite if success: if w: cmd="COMMIT TRANSACTION" else: cmd="END TRANSACTION" else: if w: cmd="ROLLBACK TRANSACTION" else: cmd="END TRANSACTION" print cmd self.cursor.execute(cmd) setattr(_transactionwrapper, "__doc__", getattr(method, "__doc__")) return _transactionwrapper def sqlquote(s): "returns an sqlite quoted string (the return value will begin and end with single quotes)" return "'"+s.replace("'", "''")+"'" def idquote(s): """returns an sqlite quoted identifier (eg for when a column name is also an SQL keyword The value returned is quoted in square brackets""" return '['+s+']' class IntegrityCheckFailed(Exception): pass class Database: def __init__(self, filename): self.connection=apsw.Connection(filename) self.cursor=self.connection.cursor() # we always do an integrity check first icheck=[] print "database integrity check" for row in self.cursor.execute("pragma integrity_check"): icheck.extend(row) print "database integrity check complete" icheck="\n".join(icheck) if icheck!="ok": raise IntegrityCheckFailed(icheck) # exclusive lock counter self.excounter=0 # this should be set to true by any code that writes - it is # used by the exclusivewrapper to tell if it should do a # commit/rollback or just a plain end self.transactionwrite=False # a cache of the table schemas self._schemacache={} self.sql=self.cursor.execute self.sqlmany=self.cursor.executemany if TRACE: self.cursor.setexectrace(self._sqltrace) self.cursor.setrowtrace(self._rowtrace) def _sqltrace(self, cmd, bindings): print "SQL:",cmd if bindings: print " bindings:",bindings return True def _rowtrace(self, *row): print "ROW:",row return row def sql(self, statement, params=()): "Executes statement and return a generator of the results" # this is replaced in init assert False def sqlmany(self, statement, params): "execute statements repeatedly with params" # this is replaced in init assert False def doestableexist(self, tablename): if tablename in self._schemacache: return True return bool(self.sql("select count(*) from sqlite_master where type='table' and name=%s" % (sqlquote(tablename),)).next()[0]) def getcolumns(self, tablename, onlynames=False): res=self._schemacache.get(tablename,None) if res is None: res=[] for colnum,name,type, _, default, primarykey in self.sql("pragma table_info("+idquote(tablename)+")"): if primarykey: type+=" primary key" res.append([colnum,name,type]) self._schemacache[tablename]=res if onlynames: return [name for colnum,name,type in res] return res def savemajordict(self, tablename, dict, timestamp=None): """This is the entrypoint for saving a first level dictionary such as the phonebook or calendar. @param tablename: name of the table to use @param dict: The dictionary of record. The key must be the uniqueid for each record. The @L{extractbitpimserials} function can do the conversion for you for phonebook and similar formatted records. @param timestamp: the UTC time in seconds since the epoch. This is """ if timestamp is None: timestamp=time.time() # work on a shallow copy of dict dict=dict.copy() # make sure the table exists first if not self.doestableexist(tablename): # create table and include meta-fields self.transactionwrite=True self.sql("create table %s (__rowid__ integer primary key, __timestamp__, __deleted__ integer, __uid__ varchar)" % (idquote(tablename),)) # get the latest values for each guid ... current=self.getmajordictvalues(tablename) # compare what we have, and update/mark deleted as appropriate ... deleted=[k for k in current if k not in dict] new=[k for k in dict if k not in current] modified=[k for k in dict if k in current] # only potentially modified ... # deal with modified first dl=[] for i,k in enumerate(modified): if dict[k]==current[k]: # unmodified! del dict[k] dl.append(i) dl.reverse() for i in dl: del modified[i] # add deleted entries back into dict for d in deleted: assert d not in dict dict[d]=current[d] dict[d]["__deleted__"]=1 # now we only have new, changed and deleted entries left in dict # examine the keys in dict dk=[] for k in dict.keys(): # make a copy since we modify values, but it doesn't matter about deleted since we own those if k not in deleted: dict[k]=dict[k].copy() for kk in dict[k]: if kk not in dk: dk.append(kk) # verify that they don't start with __ assert len([k for k in dk if k.startswith("__") and not k=="__deleted__"])==0 # get database keys dbkeys=self.getcolumns(tablename, onlynames=True) # are any missing? missing=[k for k in dk if k not in dbkeys] if len(missing): creates=[] # for each missing key, we have to work out if the value # is a list or dict type (which we indirect to another table) for m in missing: islist=None isdict=None isnotindirect=None for r in dict.keys(): record=dict[r] v=record.get(m,None) if v is None: continue if isinstance(v, list): islist=record elif isinstance(v,type({})): isdict=record else: isnotindirect=record # in devel code, we check every single value # in production, we just use the first we find if not __debug__: break if islist is None and isdict is None and isnotindirect is None: # they have the key but no record has any values, so we ignore it del dk[dk.index(m)] continue # don't do this type abuse at home ... if int(islist is not None)+int(isdict is not None)+int(isnotindirect is not None)!=int(True): # can't have it more than one way raise ValueError("key %s for table %s has values with inconsistent types. eg LIST: %s, DICT: %s, NOTINDIRECT: %s" % (m,tablename,`islist`,`isdict`,`isnotindirect`)) if islist is not None: creates.append( (m, "indirectBLOB") ) continue if isdict: creates.append( (m, "indirectdictBLOB")) continue if isnotindirect is not None: creates.append( (m, "valueBLOB") ) continue assert False, "You can't possibly get here!" if len(creates): self._altertable(tablename, creates, createindex=1) # write out indirect values dbtkeys=self.getcolumns(tablename) # for every indirect, we have to replace the value with a pointer for _,n,t in dbtkeys: if t in ("indirectBLOB", "indirectdictBLOB"): indirects={} for r in dict.keys(): record=dict[r] v=record.get(n,None) if v is not None: if not len(v): # set zero length lists/dicts to None record[n]=None else: if t=="indirectdictBLOB": indirects[r]=[v] # make it a one item dict list else: indirects[r]=v if len(indirects): self.updateindirecttable(tablename+"__"+n, indirects) for r in indirects.keys(): dict[r][n]=indirects[r] # and now the main table for k in dict.keys(): record=dict[k] record["__uid__"]=k rk=record.keys() rk.sort() cmd=["insert into", idquote(tablename), "( [__timestamp__],"] cmd.append(",".join([idquote(r) for r in rk])) cmd.extend([")", "values", "(?,"]) cmd.append(",".join(["?" for r in rk])) cmd.append(")") self.sql(" ".join(cmd), [timestamp]+[record[r] for r in rk]) self.transactionwrite=True def updateindirecttable(self, tablename, indirects): # this is mostly similar to savemajordict, except we only deal # with lists of dicts, and we find existing records with the # same value if possible # does the table even exist? if not self.doestableexist(tablename): # create table and include meta-fields self.sql("create table %s (__rowid__ integer primary key)" % (idquote(tablename),)) self.transactionwrite=True # get the list of keys from indirects datakeys=[] for i in indirects.keys(): assert isinstance(indirects[i], list) for v in indirects[i]: assert isinstance(v, dict) for k in v.keys(): if k not in datakeys: assert not k.startswith("__") datakeys.append(k) # get the keys from the table dbkeys=self.getcolumns(tablename, onlynames=True) # are any missing? missing=[k for k in datakeys if k not in dbkeys] if len(missing): self._altertable(tablename, [(m,"valueBLOB") for m in missing], createindex=2) # for each row we now work out the indirect information for r in indirects: res=tablename+"," for record in indirects[r]: cmd=["select __rowid__ from", idquote(tablename), "where"] params=[] coals=[] for d in datakeys: v=record.get(d,None) if v is None: coals.append(idquote(d)) else: if cmd[-1]!="where": cmd.append("and") cmd.extend([idquote(d), "= ?"]) params.append(v) assert cmd[-1]!="where" # there must be at least one non-none column! if len(coals)==1: cmd.extend(["and",coals[0],"isnull"]) elif len(coals)>1: cmd.extend(["and coalesce(",",".join(coals),") isnull"]) found=None for found in self.sql(" ".join(cmd), params): # get matching row found=found[0] break if found is None: # add it cmd=["insert into", idquote(tablename), "("] params=[] for k in record: if cmd[-1]!="(": cmd.append(",") cmd.append(k) params.append(record[k]) cmd.extend([")", "values", "("]) cmd.append(",".join(["?" for p in params])) cmd.append("); select last_insert_rowid()") found=self.sql(" ".join(cmd), params).next()[0] self.transactionwrite=True res+=`found`+"," indirects[r]=res def getmajordictvalues(self, tablename, factory=dictdataobjectfactory, at_time=None): if not self.doestableexist(tablename): return {} res={} uids=[u[0] for u in self.sql("select distinct __uid__ from %s" % (idquote(tablename),))] schema=self.getcolumns(tablename) for colnum,name,type in schema: if name=='__deleted__': deleted=colnum elif name=='__uid__': uid=colnum # get all relevant rows if isinstance(at_time, (int, float)): sql_string="select * from %s where __uid__=? and __timestamp__<=%f order by __rowid__ desc limit 1" % (idquote(tablename), float(at_time)) else: sql_string="select * from %s where __uid__=? order by __rowid__ desc limit 1" % (idquote(tablename),) indirects={} for row in self.sqlmany(sql_string, [(u,) for u in uids]): if row[deleted]: continue record=factory.newdataobject() for colnum,name,type in schema: if name.startswith("__") or type not in ("valueBLOB", "indirectBLOB", "indirectdictBLOB") or row[colnum] is None: continue if type=="valueBLOB": record[name]=row[colnum] continue assert type=="indirectBLOB" or type=="indirectdictBLOB" if name not in indirects: indirects[name]=[] indirects[name].append( (row[uid], row[colnum], type) ) res[row[uid]]=record # now get the indirects for name,values in indirects.iteritems(): for uid,v,type in values: if type=="indirectBLOB": res[uid][name]=self._getindirect(v) else: res[uid][name]=self._getindirect(v)[0] return res def _getindirect(self, what): """Gets a list of values (indirect) as described by what @param what: what to get - eg phonebook_serials,1,3,5, (note there is always a trailing comma) """ tablename,rows=what.split(',', 1) schema=self.getcolumns(tablename) res=[] for row in self.sqlmany("select * from %s where __rowid__=?" % (idquote(tablename),), [(int(r),) for r in rows.split(',') if len(r)]): record={} for colnum,name,type in schema: if name.startswith("__") or type not in ("valueBLOB", "indirectBLOB", "indirectdictBLOB") or row[colnum] is None: continue if type=="valueBLOB": record[name]=row[colnum] continue assert type=="indirectBLOB" or type=="indirectdictBLOB" assert False, "indirect in indirect not handled" assert len(record) res.append(record) assert len(res) return res def _altertable(self, tablename, columnstoadd, createindex=0): """Alters the named table by adding the listed columns @param tablename: name of the table to alter @param columnstoadd: a list of (name,type) of the columns to add @param createindex: what sort of index to create. 0 means none, 1 means on just __uid__ and 2 is on all data columns """ # indexes are automatically dropped when table is dropped so we don't need to dbtkeys=self.getcolumns(tablename) # clean out cache entry since we are about to invalidate it del self._schemacache[tablename] self.transactionwrite=True cmd=["create", "temporary", "table", idquote("backup_"+tablename), "("] for _,n,t in dbtkeys: if cmd[-1]!="(": cmd.append(",") cmd.append(idquote(n)) cmd.append(t) cmd.append(")") self.sql(" ".join(cmd)) # copy the values into the temporary table self.sql("insert into %s select * from %s" % (idquote("backup_"+tablename), idquote(tablename))) # drop the source table self.sql("drop table %s" % (idquote(tablename),)) # recreate the source table with new columns del cmd[1] # remove temporary cmd[2]=idquote(tablename) # change tablename del cmd[-1] # remove trailing ) for n,t in columnstoadd: cmd.extend((',', idquote(n), t)) cmd.append(')') self.sql(" ".join(cmd)) # create index if needed if createindex: if createindex==1: cmd=["create index", idquote("__index__"+tablename), "on", idquote(tablename), "(__uid__)"] elif createindex==2: cmd=["create index", idquote("__index__"+tablename), "on", idquote(tablename), "("] cols=[] for _,n,t in dbtkeys: if not n.startswith("__"): cols.append(idquote(n)) for n,t in columnstoadd: cols.append(idquote(n)) cmd.extend([",".join(cols), ")"]) else: raise ValueError("bad createindex "+`createindex`) self.sql(" ".join(cmd)) # put values back in cmd=["insert into", idquote(tablename), '('] for _,n,_ in dbtkeys: if cmd[-1]!="(": cmd.append(",") cmd.append(idquote(n)) cmd.extend([")", "select * from", idquote("backup_"+tablename)]) self.sql(" ".join(cmd)) self.sql("drop table "+idquote("backup_"+tablename)) def deleteold(self, tablename, uids=None, minvalues=3, maxvalues=5, keepoldest=93): """Deletes old entries from the database. The deletion is based on either criterion of maximum values or age of values matching. @param uids: You can limit the items deleted to this list of uids, or None for all entries. @param minvalues: always keep at least this number of values @param maxvalues: maximum values to keep for any entry (you can supply None in which case no old entries will be removed based on how many there are). @param keepoldest: values older than this number of days before now are removed. You can also supply None in which case no entries will be removed based on age. @returns: number of rows removed,number of rows remaining """ if not self.doestableexist(tablename): return (0,0) timecutoff=0 if keepoldest is not None: timecutoff=time.time()-(keepoldest*24*60*60) if maxvalues is None: maxvalues=sys.maxint-1 if uids is None: uids=[u[0] for u in self.sql("select distinct __uid__ from %s" % (idquote(tablename),))] deleterows=[] for uid in uids: deleting=False for count, (rowid, deleted, timestamp) in enumerate( self.sql("select __rowid__,__deleted__, __timestamp__ from %s where __uid__=? order by __rowid__ desc" % (idquote(tablename),), [uid])): if count<minvalues: continue if deleting: deleterows.append(rowid) continue if count>=maxvalues or timestamp<timecutoff: deleting=True if deleted: # we are ok, this is an old value now deleted, so we can remove it deleterows.append(rowid) continue # we don't want to delete current data (which may # be very old and never updated) if count>0: deleterows.append(rowid) continue self.sqlmany("delete from %s where __rowid__=?" % (idquote(tablename),), [(r,) for r in deleterows]) return len(deleterows), self.sql("select count(*) from "+idquote(tablename)).next()[0] def savelist(self, tablename, values): """Just save a list of items (eg categories). There is no versioning or transaction history. Internally the table has two fields. One is the actual value and the other indicates if the item is deleted. """ # a tuple of the quoted table name tn=(idquote(tablename),) if not self.doestableexist(tablename): self.sql("create table %s (__rowid__ integer primary key, item, __deleted__ integer)" % tn) # some code to demonstrate my lack of experience with SQL .... delete=[] known=[] revive=[] for row, item, dead in self.sql("select __rowid__,item,__deleted__ from %s" % tn): known.append(item) if item in values: # we need this row if dead: revive.append((row,)) continue if dead: # don't need this entry and it is dead anyway continue delete.append((row,)) create=[(v,) for v in values if v not in known] # update table as appropriate self.sqlmany("update %s set __deleted__=0 where __rowid__=?" % tn, revive) self.sqlmany("update %s set __deleted__=1 where __rowid__=?" % tn, delete) self.sqlmany("insert into %s (item, __deleted__) values (?,0)" % tn, create) if __debug__: vdup=values[:] vdup.sort() vv=self.loadlist(tablename) vv.sort() assert vdup==vv def loadlist(self, tablename): """Loads a list of items (eg categories)""" if not self.doestableexist(tablename): return [] return [v[0] for v in self.sql("select item from %s where __deleted__=0" % (idquote(tablename),))] # various operations need exclusive access to the database savemajordict=ExclusiveWrapper(savemajordict) getmajordictvalues=ExclusiveWrapper(getmajordictvalues) deleteold=ExclusiveWrapper(deleteold) savelist=ExclusiveWrapper(savelist) loadlist=ExclusiveWrapper(loadlist) def getchangescount(self, tablename): """Return the number of additions, deletions, and modifications made to this table over time. Expected fields containted in this table: __timestamp__,__deleted__, __uid__ Assuming that both __rowid__ and __timestamp__ values are both ascending """ if not self.doestableexist(tablename): return {} tn=idquote(tablename) # get the unique dates of changes sql_cmd='select distinct __timestamp__ from %s' % tn # setting up the return dict res={} for t in self.sql(sql_cmd): res[t[0]]={ 'add': 0, 'del': 0, 'mod': 0 } # go through the table and count the changes existing_uid={} sql_cmd='select __timestamp__,__uid__,__deleted__ from %s order by __timestamp__ asc' % tn for e in self.sql(sql_cmd): tt=e[0] uid=e[1] del_flg=e[2] if existing_uid.has_key(uid): if del_flg: res[tt]['del']+=1 del existing_uid[uid] else: res[tt]['mod']+=1 else: existing_uid[uid]=None res[tt]['add']+=1 return res if __name__=='__main__': import common import sys import time import os sys.excepthook=common.formatexceptioneh # our own hacked version for testing class phonebookdataobject(basedataobject): # no change to _knownproperties (all of ours are list properties) _knownlistproperties=basedataobject._knownlistproperties.copy() _knownlistproperties.update( {'names': ['title', 'first', 'middle', 'last', 'full', 'nickname'], 'categories': ['category'], 'emails': ['email', 'type'], 'urls': ['url', 'type'], 'ringtones': ['ringtone', 'use'], 'addresses': ['type', 'company', 'street', 'street2', 'city', 'state', 'postalcode', 'country'], 'wallpapers': ['wallpaper', 'use'], 'flags': ['secret'], 'memos': ['memo'], 'numbers': ['number', 'type', 'speeddial'], # serials is in parent object }) _knowndictproperties=basedataobject._knowndictproperties.copy() _knowndictproperties.update( {'repeat': ['daily', 'orange']} ) phonebookobjectfactory=dataobjectfactory(phonebookdataobject) # use the phonebook out of the examples directory try: execfile(os.getenv("DBTESTFILE", "examples/phonebook-index.idx")) except UnicodeError: common.unicode_execfile(os.getenv("DBTESTFILE", "examples/phonebook-index.idx")) ensurerecordtype(phonebook, phonebookobjectfactory) phonebookmaster=phonebook def testfunc(): global phonebook, TRACE, db # note that iterations increases the size of the # database/journal and will make each one take longer and # longer as the db/journal gets bigger if len(sys.argv)>=2: iterations=int(sys.argv[1]) else: iterations=1 if iterations >1: TRACE=False db=Database("testdb") b4=time.time() for i in xrange(iterations): phonebook=phonebookmaster.copy() # write it out db.savemajordict("phonebook", extractbitpimserials(phonebook)) # check what we get back is identical v=db.getmajordictvalues("phonebook") assert v==extractbitpimserials(phonebook) # do a deletion del phonebook[17] # james bond @ microsoft db.savemajordict("phonebook", extractbitpimserials(phonebook)) # and verify v=db.getmajordictvalues("phonebook") assert v==extractbitpimserials(phonebook) # modify a value phonebook[15]['addresses'][0]['city']="Bananarama" db.savemajordict("phonebook", extractbitpimserials(phonebook)) # and verify v=db.getmajordictvalues("phonebook") assert v==extractbitpimserials(phonebook) after=time.time() print "time per iteration is",(after-b4)/iterations,"seconds" print "total time was",after-b4,"seconds for",iterations,"iterations" if iterations>1: print "testing repeated reads" b4=time.time() for i in xrange(iterations*10): db.getmajordictvalues("phonebook") after=time.time() print "\ttime per iteration is",(after-b4)/(iterations*10),"seconds" print "\ttotal time was",after-b4,"seconds for",iterations*10,"iterations" print print "testing repeated writes" x=extractbitpimserials(phonebook) k=x.keys() b4=time.time() for i in xrange(iterations*10): # we remove 1/3rd of the entries on each iteration xcopy=x.copy() for l in range(i,i+len(k)/3): del xcopy[k[l%len(x)]] db.savemajordict("phonebook",xcopy) after=time.time() print "\ttime per iteration is",(after-b4)/(iterations*10),"seconds" print "\ttotal time was",after-b4,"seconds for",iterations*10,"iterations" if len(sys.argv)==3: # also run under hotspot then def profile(filename, command): import hotshot, hotshot.stats, os file=os.path.abspath(filename) profile=hotshot.Profile(file) profile.run(command) profile.close() del profile howmany=100 stats=hotshot.stats.load(file) stats.strip_dirs() stats.sort_stats('time', 'calls') stats.print_stats(100) stats.sort_stats('cum', 'calls') stats.print_stats(100) stats.sort_stats('calls', 'time') stats.print_stats(100) sys.exit(0) profile("dbprof", "testfunc()") else: testfunc()
database.py.diff
(application/octet-stream, 949 B)
Index: database.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/database.py,v
retrieving revision 1.26
diff -u -r1.26 database.py
--- database.py 12 Jul 2005 21:23:57 -0000 1.26
+++ database.py 8 Oct 2005 18:40:22 -0000
@@ -17,6 +17,7 @@
import apsw
+import common
###
### The first section of this file deals with typical objects used to
### represent data items and various methods for wrapping them.
@@ -877,7 +878,10 @@
phonebookobjectfactory=dataobjectfactory(phonebookdataobject)
# use the phonebook out of the examples directory
- execfile(os.getenv("DBTESTFILE", "examples/phonebook-index.idx"))
+ try:
+ execfile(os.getenv("DBTESTFILE", "examples/phonebook-index.idx"))
+ except UnicodeError:
+ common.unicode_execfile(os.getenv("DBTESTFILE", "examples/phonebook-index.idx"))
ensurerecordtype(phonebook, phonebookobjectfactory)
vcard.py
(text/plain, 34.7 KB)
### BITPIM ### ### Copyright (C) 2003-2004 Roger Binns <[email protected]> ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the BitPim license as detailed in the LICENSE file. ### ### $Id: vcard.py,v 1.29 2005/09/16 02:23:28 djpham Exp $ """Code for reading and writing Vcard VCARD is defined in RFC 2425 and 2426 """ import sys import quopri import base64 import codecs import cStringIO import common import nameparser import phonenumber class VFileException(Exception): pass class VFile: _charset_aliases={ 'MACINTOSH': 'MAC_ROMAN' } def __init__(self, source): self.source=source self.saved=None def __iter__(self): return self def next(self): # Get the next non-blank line while True: # python desperately needs do-while line=self._getnextline() if line is None: raise StopIteration() if len(line)!=0: break # Hack for evolution. If ENCODING is QUOTED-PRINTABLE then it doesn't # offset the next line, so we look to see what the first char is normalcontinuations=True colon=line.find(':') if colon>0: s=line[:colon].lower().split(";") if "quoted-printable" in s or 'encoding=quoted-printable' in s: normalcontinuations=False while line[-1]=="=" or line[-2]=='=': if line[-1]=='=': i=-1 else: i=-2 nextl=self._getnextline() if nextl[0] in ("\t", " "): nextl=nextl[1:] line=line[:i]+nextl while normalcontinuations: nextline=self._lookahead() if nextline is None: break if len(nextline)==0: break if nextline[0]!=' ' and nextline[0]!='\t': break line+=self._getnextline()[1:] colon=line.find(':') if colon<1: # some evolution vcards don't even have colons # raise VFileException("Invalid property: "+line) if __debug__: print "Fixing up bad line",line colon=len(line) line+=":" b4=line[:colon] line=line[colon+1:].strip() # upper case and split on semicolons items=b4.upper().split(";") newitems=[] if isinstance(line, unicode): charset=None else: charset="LATIN-1" for i in items: # ::TODO:: probably delete anything preceding a '.' # (see 5.8.2 in rfc 2425) # look for charset parameter if i.startswith("CHARSET="): charset = i[8:] or "LATIN-1" continue # unencode anything that needs it if not i.startswith("ENCODING=") and not i=="QUOTED-PRINTABLE": # evolution doesn't bother with "ENCODING=" # ::TODO:: deal with backslashes, being especially careful with ones quoting semicolons newitems.append(i) continue try: if i=='QUOTED-PRINTABLE' or i=="ENCODING=QUOTED-PRINTABLE": # technically quoted printable is ascii only but we decode anyway since not all vcards comply line=quopri.decodestring(line) elif i=='ENCODING=B': line=base64.decodestring(line) charset=None else: raise VFileException("unknown encoding: "+i) except Exception,e: if isinstance(e,VFileException): raise e raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line)) # ::TODO:: repeat above shenanigans looking for a VALUE= thingy and # convert line as in 5.8.4 of rfc 2425 if len(newitems)==0: raise VFileException("Line contains no property: %s" % (line,)) # charset frigging if charset is not None: try: decoder=codecs.getdecoder(self._charset_aliases.get(charset, charset)) line,_=decoder(line) except LookupError: raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4)) if newitems==["BEGIN"] or newitems==["END"]: line=line.upper() return newitems,line def _getnextline(self): if self.saved is not None: line=self.saved self.saved=None return line else: return self._readandstripline() def _readandstripline(self): line=self.source.readline() if line is not None: if len(line)==0: return None elif line[-2:]=="\r\n": return line[:-2] elif line[-1]=='\r' or line[-1]=='\n': return line[:-1] return line def _lookahead(self): assert self.saved is None self.saved=self._readandstripline() return self.saved class VCards: "Understands vcards in a vfile" def __init__(self, vfile): self.vfile=vfile def __iter__(self): return self def next(self): # find vcard start field=value=None for field,value in self.vfile: if (field,value)!=(["BEGIN"], "VCARD"): continue found=True break if (field,value)!=(["BEGIN"], "VCARD"): # hit eof without any BEGIN:vcard raise StopIteration() # suck up lines lines=[] for field,value in self.vfile: if (field,value)!=(["END"], "VCARD"): lines.append( (field,value) ) continue break if (field,value)!=(["END"], "VCARD"): raise VFileException("There is a BEGIN:VCARD but no END:VCARD") return VCard(lines) class VCard: "A single vcard" def __init__(self, lines): self._version=(2,0) # which version of the vcard spec the card conforms to self._origin=None # which program exported the vcard self._data={} self._groups={} self.lines=[] # extract version field for f,v in lines: assert len(f) if f==["X-EVOLUTION-FILE-AS"]: # all evolution cards have this self._origin="evolution" if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")): self._origin="apple" if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >=0: self.origin="apple" if f==["VERSION"]: ver=v.split(".") try: ver=[int(xx) for xx in ver] except ValueError: raise VFileException(v+" is not a valid vcard version") self._version=ver continue # convert {home,work}.{tel,label} to {tel,label};{home,work} # this probably dates from *very* early vcards if f[0]=="HOME.TEL": f[0:1]=["TEL", "HOME"] elif f[0]=="HOME.LABEL": f[0:1]=["LABEL", "HOME"] elif f[0]=="WORK.TEL": f[0:1]=["TEL", "WORK"] elif f[0]=="WORK.LABEL": f[0:1]=["LABEL", "WORK"] self.lines.append( (f,v) ) self._parse(self.lines, self._data) self._update_groups(self._data) def getdata(self): "Returns a dict of the data parsed out of the vcard" return self._data def _getfieldname(self, name, dict): """Returns the fieldname to use in the dict. For example, if name is "email" and there is no "email" field in dict, then "email" is returned. If there is already an "email" field then "email2" is returned, etc""" if name not in dict: return name for i in xrange(2,99999): if name+`i` not in dict: return name+`i` def _parse(self, lines, result): for field,value in lines: if len(value.strip())==0: # ignore blank values continue if '.' in field[0]: f=field[0][field[0].find('.')+1:] else: f=field[0] t=f.replace("-", "_") func=getattr(self, "_field_"+t, self._default_field) func(field, value, result) def _update_groups(self, result): """Update the groups info """ for k,e in self._groups.items(): self._setvalue(result, *e) # fields we ignore def _field_ignore(self, field, value, result): pass _field_LABEL=_field_ignore # we use the ADR field instead _field_BDAY=_field_ignore # not stored in bitpim _field_ROLE=_field_ignore # not stored in bitpim _field_CALURI=_field_ignore # not stored in bitpim _field_CALADRURI=_field_ignore # variant of above _field_FBURL=_field_ignore # not stored in bitpim _field_REV=_field_ignore # not stored in bitpim _field_KEY=_field_ignore # not stored in bitpim _field_SOURCE=_field_ignore # not stored in bitpim (although arguably part of serials) # simple fields def _field_FN(self, field, value, result): result[self._getfieldname("name", result)]=self.unquote(value) def _field_TITLE(self, field, value, result): result[self._getfieldname("title", result)]=self.unquote(value) def _field_NICKNAME(self, field, value, result): # ::TODO:: technically this is a comma seperated list .. result[self._getfieldname("nickname", result)]=self.unquote(value) def _field_NOTE(self, field, value, result): result[self._getfieldname("notes", result)]=self.unquote(value) def _field_UID(self, field, value, result): result["uid"]=self.unquote(value) # note that we only store one UID (the "U" does stand for unique) # # Complex fields # def _field_N(self, field, value, result): value=self.splitandunquote(value) familyname=givenname=additionalnames=honorificprefixes=honorificsuffixes=None try: familyname=value[0] givenname=value[1] additionalnames=value[2] honorificprefixes=value[3] honorificsuffixes=value[4] except IndexError: pass if familyname is not None and len(familyname): result[self._getfieldname("last name", result)]=familyname if givenname is not None and len(givenname): result[self._getfieldname("first name", result)]=givenname if additionalnames is not None and len(additionalnames): result[self._getfieldname("middle name", result)]=additionalnames if honorificprefixes is not None and len(honorificprefixes): result[self._getfieldname("prefix", result)]=honorificprefixes if honorificsuffixes is not None and len(honorificsuffixes): result[self._getfieldname("suffix", result)]=honorificsuffixes _field_NAME=_field_N # early versions of vcard did this def _field_ORG(self, field, value, result): value=self.splitandunquote(value) if len(value): result[self._getfieldname("organisation", result)]=value[0] for f in value[1:]: result[self._getfieldname("organisational unit", result)]=f _field_O=_field_ORG # early versions of vcard did this def _field_EMAIL(self, field, value, result): value=self.unquote(value) # work out the types types=[] for f in field[1:]: if f.startswith("TYPE="): ff=f[len("TYPE="):].split(",") else: ff=[f] types.extend(ff) # the standard doesn't specify types of "home" and "work" but # does allow for random user defined types, so we look for them type=None for t in types: if t=="HOME": type="home" if t=="WORK": type="business" if t=="X400": return # we don't want no steenking X.400 preferred="PREF" in types if type is None: self._setvalue(result, "email", value, preferred) else: addr={'email': value, 'type': type} self._setvalue(result, "email", addr, preferred) def _field_URL(self, field, value, result): # the standard doesn't specify url types or a pref type, # but we implement it anyway value=self.unquote(value) # work out the types types=[] for f in field[1:]: if f.startswith("TYPE="): ff=f[len("TYPE="):].split(",") else: ff=[f] types.extend(ff) type=None for t in types: if t=="HOME": type="home" if t=="WORK": type="business" preferred="PREF" in types if type is None: self._setvalue(result, "url", value, preferred) else: addr={'url': value, 'type': type} self._setvalue(result, "url", addr, preferred) def _field_X_SPEEDDIAL(self, field, value, result): if '.' in field[0]: group=field[0][:field[0].find('.')] else: group=None if group is None: # this has to belong to a group!! print 'speedial has no group' else: self._setgroupvalue(result, 'phone', { 'speeddial': int(value) }, group, False) def _field_TEL(self, field, value, result): value=self.unquote(value) # see if this is part of a group if '.' in field[0]: group=field[0][:field[0].find('.')] else: group=None # work out the types types=[] for f in field[1:]: if f.startswith("TYPE="): ff=f[len("TYPE="):].split(",") else: ff=[f] types.extend(ff) # type munging - we map vcard types to simpler ones munge={ "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL", "PCS": "CELL" } types=[munge.get(t, t) for t in types] # reduce types to home, work, msg, pref, voice, fax, cell, video, pager, data types=[t for t in types if t in ("HOME", "WORK", "MSG", "PREF", "VOICE", "FAX", "CELL", "VIDEO", "PAGER", "DATA")] # if type is in this list and voice not explicitly mentioned then it is not a voice type antivoice=["FAX", "PAGER", "DATA"] if "VOICE" in types: voice=True else: voice=True # default is voice for f in antivoice: if f in types: voice=False break preferred="PREF" in types # vcard allows numbers to be multiple things at the same time, such as home voice, home fax # and work fax so we have to test for all variations # if neither work or home is specified, then no default (otherwise things get really complicated) iswork=False ishome=False if "WORK" in types: iswork=True if "HOME" in types: ishome=True if len(types)==0 or types==["PREF"]: iswork=True # special case when nothing else is specified value=phonenumber.normalise(value) if iswork and voice: self._setgroupvalue(result, "phone", {"type": "business", "number": value}, group, preferred) if ishome and voice: self._setgroupvalue(result, "phone", {"type": "home", "number": value}, group, preferred) if not iswork and not ishome and "FAX" in types: # fax without explicit work or home self._setgroupvalue(result, "phone", {"type": "fax", "number": value}, group, preferred) else: if iswork and "FAX" in types: self._setgroupvalue(result, "phone", {"type": "business fax", "number": value}, group, preferred) if ishome and "FAX" in types: self._setgroupvalue(result, "phone", {"type": "home fax", "number": value}, group, preferred) if "CELL" in types: self._setgroupvalue(result, "phone", {"type": "cell", "number": value}, group, preferred) if "PAGER" in types: self._setgroupvalue(result, "phone", {"type": "pager", "number": value}, group, preferred) if "DATA" in types: self._setgroupvalue(result, "phone", {"type": "data", "number": value}, group, preferred) def _setgroupvalue(self, result, type, value, group, preferred=False): """ Set value of an item of a group """ if group is None: # no groups specified return self._setvalue(result, type, value, preferred) group_type=self._groups.get(group, None) if group_type is None: # 1st one of the group self._groups[group]=[type, value, preferred] else: if type!=group_type[0]: print 'Group',group,'has different types:',type,groups_type[0] if preferred: group_type[2]=True group_type[1].update(value) def _setvalue(self, result, type, value, preferred=False): if type not in result: result[type]=value return if not preferred: result[self._getfieldname(type, result)]=value return # we need to insert our value at the begining values=[value] for suffix in [""]+range(2,99): if type+str(suffix) in result: values.append(result[type+str(suffix)]) else: break suffixes=[""]+range(2,len(values)+1) for l in range(len(suffixes)): result[type+str(suffixes[l])]=values[l] def _field_CATEGORIES(self, field, value, result): # comma seperated just for fun values=self.splitandunquote(value, seperator=",") values=[v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field values=[v for v in values if len(v)] v=result.get('categories', None) if v: result['categories']=';'.join([v, ";".join(values)]) else: result['categories']=';'.join(values) def _field_PHOTO(self, field, value, result): # comma seperated just for fun values=self.splitandunquote(value, seperator=",") values=[v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field values=[v for v in values if len(v)] result[self._getfieldname("wallpapers", result)]=";".join(values) def _field_SOUND(self, field, value, result): # comma seperated just for fun values=self.splitandunquote(value, seperator=",") values=[v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field values=[v for v in values if len(v)] result[self._getfieldname("ringtones", result)]=";".join(values) _field_CATEGORY=_field_CATEGORIES # apple use "category" which is not in the spec def _field_ADR(self, field, value, result): # work out the type preferred=False type="business" for f in field[1:]: if f.startswith("TYPE="): ff=f[len("TYPE="):].split(",") else: ff=[f] for x in ff: if x=="HOME": type="home" if x=="PREF": preferred=True value=self.splitandunquote(value) pobox=extendedaddress=streetaddress=locality=region=postalcode=country=None try: pobox=value[0] extendedaddress=value[1] streetaddress=value[2] locality=value[3] region=value[4] postalcode=value[5] country=value[6] except IndexError: pass addr={} if pobox is not None and len(pobox): addr["pobox"]=pobox if extendedaddress is not None and len(extendedaddress): addr["street2"]=extendedaddress if streetaddress is not None and len(streetaddress): addr["street"]=streetaddress if locality is not None and len(locality): addr["city"]=locality if region is not None and len(region): addr["state"]=region if postalcode is not None and len(postalcode): addr["postalcode"]=postalcode if country is not None and len(country): addr["country"]=country if len(addr): addr["type"]=type self._setvalue(result, "address", addr, preferred) def _field_X_PALM(self, field, value, result): # handle a few PALM custom fields ff=field[0].split(".") f0=ff[0] f1=len(ff)>1 and ff[1] or '' if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'): self._field_CATEGORIES(['CATEGORIES'], value, result) elif f0=='X-PALM-NICKNAME' or f1=='X-PALM-NICKNAME': self._field_NICKNAME(['NICKNAME'], value, result) else: if __debug__: print 'ignoring PALM custom field',field def _default_field(self, field, value, result): ff=field[0].split(".") f0=ff[0] f1=len(ff)>1 and ff[1] or '' if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'): self._field_X_PALM(field, value, result) return elif f0.startswith("X-") or f1.startswith("X-"): if __debug__: print "ignoring custom field",field return if __debug__: print "no idea what do with" print "field",field print "value",value[:80] def unquote(self, value): # ::TODO:: do this properly (deal with all backslashes) return value.replace(r"\;", ";") \ .replace(r"\,", ",") \ .replace(r"\n", "\n") \ .replace(r"\r\n", "\r\n") \ .replace("\r\n", "\n") \ .replace("\r", "\n") def splitandunquote(self, value, seperator=";"): # also need a splitandsplitandunquote since some ; delimited fields are then comma delimited # short cut for normal case - no quoted seperators if value.find("\\"+seperator)<0: return [self.unquote(v) for v in value.split(seperator)] # funky quoting, do it the slow hard way res=[] build="" v=0 while v<len(value): if value[v]==seperator: res.append(build) build="" v+=1 continue if value[v]=="\\": build+=value[v:v+2] v+=2 continue build+=value[v] v+=1 if len(build): res.append(build) return [self.unquote(v) for v in res] def version(self): "Best guess as to vcard version" return self._version def origin(self): "Best guess as to what program wrote the vcard" return self._origin def __repr__(self): str="Version: %s\n" % (`self.version()`) str+="Origin: %s\n" % (`self.origin()`) str+=common.prettyprintdict(self._data) # str+=`self.lines` return str+"\n" ### ### Outputting functions ### # The formatters return a string def myqpencodestring(value): """My own routine to do qouted printable since the builtin one doesn't encode CR or NL!""" return quopri.encodestring(value).replace("\r", "=0D").replace("\n", "=0A") def format_stringv2(value): """Return a vCard v2 string. Any embedded commas or semi-colons are removed.""" return value.replace("\\", "").replace(",", "").replace(";", "") def format_stringv3(value): """Return a vCard v3 string. Embedded commas and semi-colons are backslash quoted""" return value.replace("\\", "").replace(",", r"\,").replace(";", r"\;") _string_formatters=(format_stringv2, format_stringv3) def format_binary(value): """Return base 64 encoded string""" # encodestring always adds a newline so we have to strip it off return base64.encodestring(value).rstrip() def _is_sequence(v): """Determine if v is a sequence such as passed to value in out_line. Note that a sequence of chars is not a sequence for our purposes.""" return isinstance(v, (type( () ), type([]))) def out_line(name, attributes, value, formatter, join_char=";"): """Returns a single field correctly formatted and encoded (including trailing newline) @param name: The field name @param attributes: A list of string attributes (eg "TYPE=intl,post" ). Usually empty except for TEL and ADR. You can also pass in None. @param value: The field value. You can also pass in a list of components which will be joined with join_char such as the 6 components of N @param formatter: The function that formats the value/components. See the various format_ functions. They will automatically ensure that ENCODING=foo attributes are added if appropriate""" if attributes is None: attributes=[] # ensure it is a list else: attributes=list(attributes[:]) # ensure we work with a copy if formatter in _string_formatters: if _is_sequence(value): qp=False for f in value: f=formatter(f) if myqpencodestring(f)!=f: qp=True break if qp: attributes.append("ENCODING=QUOTED-PRINTABLE") value=[myqpencodestring(f) for f in value] value=join_char.join(value) else: value=formatter(value) # do the qp test qp= myqpencodestring(value)!=value if qp: value=myqpencodestring(value) attributes.append("ENCODING=QUOTED-PRINTABLE") else: assert not _is_sequence(value) if formatter is not None: value=formatter(value) # ::TODO:: deal with binary and other formatters and their encoding types res=";".join([name]+attributes)+":" res+=_line_reformat(value, 70, 70-len(res)) assert res[-1]!="\n" return res+"\n" def _line_reformat(line, width=70, firstlinewidth=0): """Takes line string and inserts newlines and spaces on following continuation lines so it all fits in width characters @param width: how many characters to fit it in @param firstlinewidth: if >0 then first line is this width. if equal to zero then first line is same width as rest. if <0 then first line will go immediately to continuation. """ if firstlinewidth==0: firstlinewidth=width if len(line)<firstlinewidth: return line res="" if firstlinewidth>0: res+=line[:firstlinewidth] line=line[firstlinewidth:] while len(line): res+="\n "+line[:width] if len(line)<width: break line=line[width:] return res def out_names(vals, formatter, limit=1): res="" for v in vals[:limit]: # full name res+=out_line("FN", None, nameparser.formatsimplename(v), formatter) # name parts f,m,l=nameparser.getparts(v) res+=out_line("N", None, (l,f,m,"",""), formatter) # nickname nn=v.get("nickname", "") if len(nn): res+=out_line("NICKNAME", None, nn, formatter) return res # Apple uses wrong field name so we do some futzing ... def out_categories(vals, formatter, field="CATEGORIES"): cats=[v.get("category") for v in vals] if len(cats): return out_line(field, None, cats, formatter, join_char=",") return "" def out_categories_apple(vals, formatter): return out_categories(vals, formatter, field="CATEGORY") # Used for both email and urls. we don't put any limits on how many are output def out_eu(vals, formatter, field, bpkey): res="" first=True for v in vals: val=v.get(bpkey) type=v.get("type", "") if len(type): if type=="business": type="work" # vcard uses different name type=type.upper() if first: type=type+",PREF" elif first: type="PREF" if len(type): type=["TYPE="+type+["",",INTERNET"][field=="EMAIL"]] # email also has "INTERNET" else: type=None res+=out_line(field, type, val, formatter) first=False return res def out_emails(vals, formatter): return out_eu(vals, formatter, "EMAIL", "email") def out_urls(vals, formatter): return out_eu(vals, formatter, "URL", "url") # fun fun fun _out_tel_mapping={ 'home': 'HOME', 'office': 'WORK', 'cell': 'CELL', 'fax': 'FAX', 'pager': 'PAGER', 'data': 'MODEM', 'none': 'VOICE' } def out_tel(vals, formatter): # ::TODO:: limit to one type of each number phones=['phone'+str(x) for x in ['']+range(2,len(vals)+1)] res="" first=True idx=0 for v in vals: sp=v.get('speeddial', None) if sp is None: # no speed dial res+=out_line("TEL", ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])], phonenumber.format(v['number']), formatter) else: res+=out_line(phones[idx]+".TEL", ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])], phonenumber.format(v['number']), formatter) res+=out_line(phones[idx]+".X-SPEEDDIAL", None, str(sp), formatter) idx+=1 first=False return res # and addresses def out_adr(vals, formatter): # ::TODO:: limit to one type of each address, and only one org res="" first=True for v in vals: o=v.get("company", "") if len(o): res+=out_line("ORG", None, o, formatter) if v.get("type")=="home": type="HOME" else: type="WORK" type="TYPE="+type+("", ",PREF")[first] res+=out_line("ADR", [type], [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], formatter) first=False return res def out_note(vals, formatter, limit=1): return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]]) def out_wallpapers(vals, formatter): w=[v.get("wallpaper") for v in vals] if len(w): return out_line('PHOTO', None, w, formatter, join_char=",") return "" def out_ringtones(vals, formatter): r=[v.get("ringtone") for v in vals] if len(r): return out_line('SOUND', None, r, formatter, join_char=",") return "" # This is the order we write things out to the vcard. Although # vCard doesn't require an ordering, it looks nicer if it # is (eg name first) _field_order=("names", "wallpapers", "addresses", "numbers", "categories", "emails", "urls", "ringtones", "flags", "memos", "serials") def output_entry(entry, profile, limit_fields=None): # debug build assertion that limit_fields only contains fields we know about if __debug__ and limit_fields is not None: assert len([f for f in limit_fields if f not in _field_order])==0 fmt=profile["_formatter"] io=cStringIO.StringIO() io.write(out_line("BEGIN", None, "VCARD", None)) io.write(out_line("VERSION", None, profile["_version"], None)) if limit_fields is None: fields=_field_order else: fields=[f for f in _field_order if f in limit_fields] for f in fields: if f in entry and f in profile: func=profile[f] # does it have a limit? (nice scary introspection :-) if "limit" in func.func_code.co_varnames[:func.func_code.co_argcount]: lines=func(entry[f], fmt, limit=profile["_limit"]) else: lines=func(entry[f], fmt) if len(lines): io.write(lines) io.write(out_line("END", None, "VCARD", fmt)) return io.getvalue() profile_vcard2={ '_formatter': format_stringv2, '_limit': 1, '_version': "2.1", 'names': out_names, 'categories': out_categories, 'emails': out_emails, 'urls': out_urls, 'numbers': out_tel, 'addresses': out_adr, 'memos': out_note, 'wallpapers': out_wallpapers, 'ringtones': out_ringtones } profile_vcard3=profile_vcard2.copy() profile_vcard3['_formatter']=format_stringv3 profile_vcard3['_version']="3.0" profile_apple=profile_vcard3.copy() profile_apple['categories']=out_categories_apple profile_full=profile_vcard3.copy() profile_full['_limit']=99999 profiles={ 'vcard2': { 'description': "vCard v2.1", 'profile': profile_vcard2 }, 'vcard3': { 'description': "vCard v3.0", 'profile': profile_vcard3 }, 'apple': { 'description': "Apple", 'profile': profile_apple }, 'fullv3': { 'description': "Full vCard v3.0", 'profile': profile_full}, } if __name__=='__main__': def _wrap(func): try: return func() except: print common.formatexception() sys.exit(1) def dump_vcards(): for vcard in VCards(VFile(common.opentextfile(sys.argv[1]))): # pass print vcard def turn_around(): p="fullv3" if len(sys.argv)==4: p=sys.argv[4] print "Using profile", profiles[p]['description'] profile=profiles[p]['profile'] d={'result': {}} try: execfile(sys.argv[1], d,d) except UnicodeError: common.unicode_execfile(sys.argv[1], d,d) f=open(sys.argv[2], "wt") for k in d['result']['phonebook']: print >>f, output_entry(d['result']['phonebook'][k], profile) f.close() if len(sys.argv)==2: # import bp # bp.profile("vcard.prof", "dump_vcards()") _wrap(dump_vcards) elif len(sys.argv)==3 or len(sys.argv)==4: _wrap(turn_around) else: print """one arg: import the named vcard file two args: first arg is phonebook/index.idx file, write back out to arg2 in vcard format three args: same as two but last arg is profile to use. profiles are""", profiles.keys()
vcard.py.diff
(application/octet-stream, 695 B)
Index: vcard.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/vcard.py,v
retrieving revision 1.29
diff -u -r1.29 vcard.py
--- vcard.py 16 Sep 2005 02:23:28 -0000 1.29
+++ vcard.py 8 Oct 2005 18:40:22 -0000
@@ -968,8 +968,11 @@
profile=profiles[p]['profile']
d={'result': {}}
- execfile(sys.argv[1], d,d)
-
+ try:
+ execfile(sys.argv[1], d,d)
+ except UnicodeError:
+ common.unicode_execfile(sys.argv[1], d,d)
+
f=open(sys.argv[2], "wt")
for k in d['result']['phonebook']:
print >>f, output_entry(d['result']['phonebook'][k], profile)