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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.