=== modified file 'conduit/datatypes/Contact.py'
--- conduit/datatypes/Contact.py	2008-02-04 20:37:49 +0000
+++ conduit/datatypes/Contact.py	2008-04-10 00:23:51 +0000
@@ -1,6 +1,5 @@
 import vobject 
 import conduit.datatypes.DataType as DataType
-
 def parse_vcf(string):
     """
     Parses a vcf string, potentially containing many vcards
@@ -21,6 +20,7 @@
     def __init__(self, **kwargs):
         DataType.DataType.__init__(self)
         self.vcard = kwargs.get('vcard',vobject.vCard())
+      #  self.g_data = ''
 
     def set_from_vcard_string(self, string):
         self.vcard = vobject.readOne(string)
@@ -58,4 +58,27 @@
         
     def get_hash(self):
         return hash(self.get_vcard_string())
+    
+    #def set_gdata(self, g_data):
+        #import gdata
+        #self.g_data = g_data
+        #vc = vCard.VCard()
+        #vc.addField(vCard.VersionLine,"3.0")
+        #vc.addField(vCard.UidLine,g_data.id.text)
+        #for l in g_data.content.text.splitlines():
+            #isinstance(l,str)
+            #if 'Phone' in l or 'phone' in l or 'Fax' in l or 'fax' in l:
+                #vc.addField(vCard.TelLine,l)
+            #elif 'Address' in l or 'address' in l:
+                #vc.addField(vCard.AdrLine,l)
+            
+        #vc.addField(vCard.FnLine, g_data.title.text)
+        #vc.
+        #vc.prettyPrint()
+        #self.set_from_vcard_string(vc)
+                    
+    #def get_gdata(self):
+        #return self.g_data
+
+    
 

=== modified file 'conduit/modules/FlickrModule/FlickrModule.py'
--- conduit/modules/FlickrModule/FlickrModule.py	2008-04-07 08:57:47 +0000
+++ conduit/modules/FlickrModule/FlickrModule.py	2008-04-10 00:23:51 +0000
@@ -221,7 +221,9 @@
         title = str(photoInfo.photo[0].title[0].elementText)
         # get tags
         tagsNode = photoInfo.photo[0].tags[0]
-
+        # get caption
+        caption = photoInfo.photo[0].description[0].elementText
+        
         if hasattr(tagsNode, 'tag'):
             tags = tuple(tag.elementText for tag in tagsNode.tag)
         else:
@@ -230,6 +232,7 @@
         # create the file
         f = Photo.Photo (URI=url)
         f.set_open_URI(url)
+        f.set_caption(caption)
 
         # try to rename if a title is available
         # FIXME: this is far from optimal, also there should be 

=== modified file 'conduit/modules/GoogleModule/GoogleModule.py'
--- conduit/modules/GoogleModule/GoogleModule.py	2008-04-07 08:57:47 +0000
+++ conduit/modules/GoogleModule/GoogleModule.py	2008-04-10 04:43:17 +0000
@@ -3,6 +3,8 @@
 import datetime
 import dateutil.parser
 import vobject
+import operator
+import time
 from dateutil.tz import tzutc, tzlocal
 from gettext import gettext as _
 import logging
@@ -14,9 +16,11 @@
 import conduit.utils as Utils
 import conduit.Exceptions as Exceptions
 from conduit.datatypes import Rid
+import conduit.datatypes.Contact as Contact
 import conduit.datatypes.Event as Event
 import conduit.datatypes.Photo as Photo
 import conduit.datatypes.Video as Video
+import conduit.datatypes.File as File
 
 #Distributors, if you ship python gdata >= 1.0.10 then remove this line
 #and the appropriate directories
@@ -33,7 +37,8 @@
     MODULES = {
         "GoogleCalendarTwoWay" : { "type": "dataprovider" },
         "PicasaTwoWay" :         { "type": "dataprovider" },
-        "YouTubeSource" :        { "type": "dataprovider" },       
+        "YouTubeSource" :        { "type": "dataprovider" },    
+        "ContactsSource" :       { "type": "dataprovider" },    
     }
     log.info("Module Information: %s" % Utils.get_module_information(gdata, None))
 except (ImportError, AttributeError):
@@ -117,6 +122,8 @@
         return '/calendar/feeds/' + self.get_uri() + '/private/full'
 
 def convert_madness_to_datetime(inputDate):
+    log.debug('Attempting to parse the following: %s'%inputDate)
+    logging.debug('Attempting to parse the following: %s'%inputDate)
     dateStr = None
     dateDate = None
     dateDateTime = None
@@ -142,6 +149,9 @@
         
     if dateDateTime is not None:
         if dateDateTime.tzinfo is not None:
+            logging.warn("returning: %s",dateDateTime)
+            ts = dateDateTime.timetuple()
+            dateDateTime = dateDateTime.fromtimestamp(time.mktime(ts))
             return dateDateTime
         elif dateTZInfo is not None:
             return dateDateTime.replace(tzinfo=dateTZInfo)
@@ -149,6 +159,7 @@
             log.warn('Waring, assuming datetime ('+dateDateTime.isoformat()+') is UTC')
             return dateDateTime.replace(tzinfo=tzutc())
                 
+    
     raise TypeError('Unable to convert to datetime')
 
 def parse_google_recur(recurString, args):
@@ -739,6 +750,113 @@
             return False
         return True
 
+
+class ContactsSource(GoogleBase, DataProvider.DataSource):
+    """
+    Contacts GData provider
+    """
+    _name_ = _("Google Contacts")
+    _description_ = _("Sync contacts from Google")
+    _category_ = conduit.dataproviders.CATEGORY_MISC
+    _module_type_ = "source"
+    _out_type_ = "contact"
+    _icon_ = "contact"
+
+    FEED = "http://www.google.com/m8/feeds/contacts/%s/base?max-results=25000"
+
+    def __init__(self, *args):
+        GoogleBase.__init__(self)
+        DataProvider.DataSource.__init__(self)
+        self.entries = None
+        self.feed = ""
+
+    def _set_username(self, username):
+        GoogleBase._set_username(self, username)
+        self.feed = self.FEED % self.username
+
+    def _do_login(self):
+        self.service = gdata.service.GDataService(service="cp", server="www.google.com")
+        self.service.ClientLogin(self.username, self.password)
+
+    def get_all(self):
+        # we can ask the server for everything thats changed after a certain date, including deletions
+        self._login()
+        contacts = self.service.GetFeed(self.feed).entry
+        return [str(contact.id.text) for contact in contacts]
+
+    def get(self, LUID):
+        DataProvider.DataSource.get(self, LUID)
+        self._login()
+        gdata = self.service.Get(LUID)
+        #print str(gdata)
+        c = Contact.Contact()
+        c.set_UID(LUID)
+        c.set_mtime(convert_madness_to_datetime(gdata.updated.text))
+        c.set_from_vcard_string(self._gdata_to_vcard(gdata))
+        c.set_open_URI(gdata.link[1].href)
+        return c
+
+    def delete(self, LUID):
+        self._login()
+        self.service.Delete(LUID)
+
+    def finish(self, aborted, error, conflict):
+        DataProvider.DataSource.finish(self)
+
+    def _gdata_to_vcard(self, g_data):
+        if g_data is None or len(g_data.ToString()) < 1:
+            return ""
+        vo = vobject.vCard()
+        vo.add('n')
+        vo.n.value = vobject.vcard.Name(given="%s"%g_data.title.text)
+        
+        vo.add('fn')
+        vo.fn.value = "%s"%g_data.title.text
+        isinstance(g_data, atom.Entry)
+        ee_names = map(operator.attrgetter('tag'),g_data.extension_elements)
+        if len(g_data.extension_elements) >0:
+            for e in [e for e in ee_names if e == 'email']:
+                vo.add('email')
+                vo.email.value = g_data.extension_elements[ee_names.index('email')].attributes['address']
+                vo.email.type_param = 'INTERNET'
+            for e in [e for e in ee_names if e == 'phoneNumber']:
+                vo.add('tel')
+                vo.tel.value = g_data.extension_elements[ee_names.index('phoneNumber')].text
+                vo.tel.type_param = g_data.extension_elements[ee_names.index('phoneNumber')].attributes['rel'].split('#')[1]
+            for e in [e for e in ee_names if e == 'postalAddress']:
+                vo.add('adr')
+                vo.adr.value = vobject.vcard.Address(g_data.extension_elements[ee_names.index('postalAddress')].text)
+               # vo.adr.value =
+                vo.adr.type_param = g_data.extension_elements[ee_names.index('postalAddress')].attributes['rel'].split('#')[1]
+        
+        #vo.add('uid').value = g_data.id.text
+        return vo.serialize(validate=True)
+        
+    def configure(self, window):
+        """
+        Configures the PicasaTwoWay
+        """
+        widget = Utils.dataprovider_glade_get_widget(
+                        __file__, 
+                        "contacts-config.glade", 
+                        "GoogleContactsConfigDialog")
+                        
+        #get a whole bunch of widgets
+        username = widget.get_widget("username")
+        password = widget.get_widget("password")
+        
+        #preload the widgets        
+        username.set_text(self.username)
+        password.set_text(self.password)
+        
+        dlg = widget.get_widget("GoogleContactsConfigDialog")
+        response = Utils.run_dialog (dlg, window)
+        if response == True:
+            self._set_username(username.get_text())
+            self._set_password(password.get_text())
+        dlg.destroy()    
+
+
 class YouTubeSource(DataProvider.DataSource):
     """
     Downloads YouTube videos using the gdata API.

=== modified file 'conduit/modules/GoogleModule/Makefile.am'
--- conduit/modules/GoogleModule/Makefile.am	2008-01-28 07:22:50 +0000
+++ conduit/modules/GoogleModule/Makefile.am	2008-04-10 00:23:51 +0000
@@ -3,8 +3,8 @@
 conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule
 conduit_handlers_PYTHON = GoogleModule.py
 
-conduit_handlers_DATA = calendar-config.glade picasa-config.glade youtube-config.glade
-EXTRA_DIST = calendar-config.glade picasa-config.glade youtube-config.glade
+conduit_handlers_DATA = calendar-config.glade picasa-config.glade youtube-config.glade contacts-config.glade
+EXTRA_DIST = calendar-config.glade picasa-config.glade youtube-config.glade contacts-config.glade
 
 clean-local:
 	rm -rf *.pyc *.pyo

=== added file 'conduit/modules/GoogleModule/contacts-config.glade'
--- conduit/modules/GoogleModule/contacts-config.glade	1970-01-01 00:00:00 +0000
+++ conduit/modules/GoogleModule/contacts-config.glade	2008-04-10 00:23:51 +0000
@@ -0,0 +1,199 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkDialog" id="GoogleContactsConfigDialog">
+  <property name="visible">True</property>
+  <property name="title" translatable="yes">Google Contacts</property>
+  <property name="type">GTK_WINDOW_TOPLEVEL</property>
+  <property name="window_position">GTK_WIN_POS_NONE</property>
+  <property name="modal">False</property>
+  <property name="default_width">250</property>
+  <property name="default_height">350</property>
+  <property name="resizable">False</property>
+  <property name="destroy_with_parent">False</property>
+  <property name="decorated">True</property>
+  <property name="skip_taskbar_hint">False</property>
+  <property name="skip_pager_hint">False</property>
+  <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+  <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+  <property name="focus_on_map">True</property>
+  <property name="urgency_hint">False</property>
+  <property name="has_separator">True</property>
+
+  <child internal-child="vbox">
+    <widget class="GtkVBox" id="vbox30">
+      <property name="visible">True</property>
+      <property name="homogeneous">False</property>
+      <property name="spacing">5</property>
+
+      <child internal-child="action_area">
+	<widget class="GtkHButtonBox" id="hbuttonbox12">
+	  <property name="visible">True</property>
+	  <property name="layout_style">GTK_BUTTONBOX_END</property>
+
+	  <child>
+	    <widget class="GtkButton" id="button32">
+	      <property name="visible">True</property>
+	      <property name="can_default">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label">gtk-cancel</property>
+	      <property name="use_stock">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="response_id">-6</property>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkButton" id="okBtn">
+	      <property name="visible">True</property>
+	      <property name="can_default">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label">gtk-ok</property>
+	      <property name="use_stock">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="response_id">-5</property>
+	    </widget>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">True</property>
+	  <property name="pack_type">GTK_PACK_END</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkLabel" id="label74">
+	  <property name="visible">True</property>
+	  <property name="label" translatable="yes">&lt;b&gt;Account Details&lt;/b&gt;</property>
+	  <property name="use_underline">False</property>
+	  <property name="use_markup">True</property>
+	  <property name="justify">GTK_JUSTIFY_LEFT</property>
+	  <property name="wrap">False</property>
+	  <property name="selectable">False</property>
+	  <property name="xalign">0.5</property>
+	  <property name="yalign">0.5</property>
+	  <property name="xpad">0</property>
+	  <property name="ypad">0</property>
+	  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+	  <property name="width_chars">-1</property>
+	  <property name="single_line_mode">False</property>
+	  <property name="angle">0</property>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkLabel" id="label75">
+	  <property name="visible">True</property>
+	  <property name="label" translatable="yes">Email:</property>
+	  <property name="use_underline">False</property>
+	  <property name="use_markup">False</property>
+	  <property name="justify">GTK_JUSTIFY_LEFT</property>
+	  <property name="wrap">False</property>
+	  <property name="selectable">False</property>
+	  <property name="xalign">0</property>
+	  <property name="yalign">0.5</property>
+	  <property name="xpad">0</property>
+	  <property name="ypad">0</property>
+	  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+	  <property name="width_chars">-1</property>
+	  <property name="single_line_mode">False</property>
+	  <property name="angle">0</property>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkEntry" id="username">
+	  <property name="visible">True</property>
+	  <property name="can_focus">True</property>
+	  <property name="editable">True</property>
+	  <property name="visibility">True</property>
+	  <property name="max_length">0</property>
+	  <property name="text" translatable="yes"></property>
+	  <property name="has_frame">True</property>
+	  <property name="invisible_char">●</property>
+	  <property name="activates_default">False</property>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkLabel" id="label76">
+	  <property name="visible">True</property>
+	  <property name="label" translatable="yes">Password:</property>
+	  <property name="use_underline">False</property>
+	  <property name="use_markup">False</property>
+	  <property name="justify">GTK_JUSTIFY_LEFT</property>
+	  <property name="wrap">False</property>
+	  <property name="selectable">False</property>
+	  <property name="xalign">0</property>
+	  <property name="yalign">0.5</property>
+	  <property name="xpad">0</property>
+	  <property name="ypad">0</property>
+	  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+	  <property name="width_chars">-1</property>
+	  <property name="single_line_mode">False</property>
+	  <property name="angle">0</property>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkEntry" id="password">
+	  <property name="visible">True</property>
+	  <property name="can_focus">True</property>
+	  <property name="editable">True</property>
+	  <property name="visibility">False</property>
+	  <property name="max_length">0</property>
+	  <property name="text" translatable="yes"></property>
+	  <property name="has_frame">True</property>
+	  <property name="invisible_char">●</property>
+	  <property name="activates_default">False</property>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkHButtonBox" id="hbuttonbox13">
+	  <property name="visible">True</property>
+	  <property name="layout_style">GTK_BUTTONBOX_END</property>
+	  <property name="spacing">0</property>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+    </widget>
+  </child>
+</widget>
+
+</glade-interface>

=== modified file 'test/python-tests/TestDataProviderGoogle.py'
--- test/python-tests/TestDataProviderGoogle.py	2008-03-26 09:16:32 +0000
+++ test/python-tests/TestDataProviderGoogle.py	2008-04-10 05:29:44 +0000
@@ -58,6 +58,21 @@
         name="event"
         )
         
+#setup the test
+test = SimpleTest(sourceName="ContactSource")
+config = {
+    "username":     os.environ["TEST_USERNAME"],
+    "password":     os.environ["TEST_PASSWORD"],
+}
+test.configure(source=config)
+google = test.get_source().module
+
+#check we can get a contacts list
+found = False
+assert(len(google.get_all())>0)
+assert(google.get(google.get_all()[0]) is not None)
+
+
 #Now a very simple youtube test...
 test = SimpleTest(sourceName="YouTubeSource")
 config = {

