1# -*- coding: utf-8 -*- 2 3# Copyright(C) 2010-2011 Romain Bignon 4# 5# This file is part of weboob. 6# 7# weboob is free software: you can redistribute it and/or modify 8# it under the terms of the GNU Lesser General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# weboob is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU Lesser General Public License for more details. 16# 17# You should have received a copy of the GNU Lesser General Public License 18# along with weboob. If not, see <http://www.gnu.org/licenses/>. 19 20from collections import OrderedDict 21from datetime import datetime 22 23from weboob.tools.compat import unicode, basestring 24from dateutil import rrule 25 26from .base import Capability, BaseObject, Field, StringField, BytesField, IntField, \ 27 BoolField, UserError 28from .address import PostalAddress, compat_field 29 30 31__all__ = [ 32 'ProfileNode', 'ContactPhoto', 'Contact', 'QueryError', 'Query', 'CapContact', 33 'BaseContact', 'PhysicalEntity', 'Person', 'Place', 'OpeningHours', 34 'OpeningRule', 'RRuleField', 'CapDirectory', 35] 36 37 38class ProfileNode(object): 39 """ 40 Node of a :class:`Contact` profile. 41 """ 42 HEAD = 0x01 43 SECTION = 0x02 44 45 def __init__(self, name, label, value, sufix=None, flags=0): 46 self.name = name 47 self.label = label 48 self.value = value 49 self.sufix = sufix 50 self.flags = flags 51 52 def __getitem__(self, key): 53 return self.value[key] 54 55 56class ContactPhoto(BaseObject): 57 """ 58 Photo of a contact. 59 """ 60 name = StringField('Name of the photo') 61 data = BytesField('Data of photo') 62 thumbnail_url = StringField('Direct URL to thumbnail') 63 thumbnail_data = BytesField('Data of thumbnail') 64 hidden = BoolField('True if the photo is hidden on website') 65 66 def __init__(self, name, url=None): 67 super(ContactPhoto, self).__init__(name, url) 68 self.name = name 69 70 def __iscomplete__(self): 71 return (self.data and (not self.thumbnail_url or self.thumbnail_data)) 72 73 def __unicode__(self): 74 return self.url 75 76 def __repr__(self): 77 return '<ContactPhoto %r data=%do tndata=%do>' % (self.id, 78 len(self.data) if self.data else 0, 79 len(self.thumbnail_data) if self.thumbnail_data else 0) 80 81 82class BaseContact(BaseObject): 83 """ 84 This is the blase class for a contact. 85 """ 86 name = StringField('Name of contact') 87 phone = StringField('Phone number') 88 email = StringField('Contact email') 89 website = StringField('Website URL of the contact') 90 91 92class Advisor(BaseContact): 93 """ 94 An advisor. 95 """ 96 email = StringField('Mail of advisor') 97 phone = StringField('Phone number of advisor') 98 mobile = StringField('Mobile number of advisor') 99 fax = StringField('Fax number of advisor') 100 agency = StringField('Name of agency') 101 address = StringField('Address of agency') 102 role = StringField('Role of advisor', default="bank") 103 104 105class Contact(BaseContact): 106 """ 107 A contact. 108 """ 109 STATUS_ONLINE = 0x001 110 STATUS_AWAY = 0x002 111 STATUS_OFFLINE = 0x004 112 STATUS_ALL = 0xfff 113 114 status = IntField('Status of contact (STATUS_* constants)') 115 status_msg = StringField('Message of status') 116 summary = StringField('Description of contact') 117 photos = Field('List of photos', dict, default=OrderedDict()) 118 profile = Field('Contact profile', dict, default=OrderedDict()) 119 120 def __init__(self, id, name, status, url=None): 121 super(Contact, self).__init__(id, url) 122 self.name = name 123 self.status = status 124 125 def set_photo(self, name, **kwargs): 126 """ 127 Set photo of contact. 128 129 :param name: name of photo 130 :type name: str 131 :param kwargs: See :class:`ContactPhoto` to know what other parameters you can use 132 """ 133 if name not in self.photos: 134 self.photos[name] = ContactPhoto(name) 135 136 photo = self.photos[name] 137 for key, value in kwargs.items(): 138 setattr(photo, key, value) 139 140 def get_text(self): 141 def print_node(node, level=1): 142 result = u'' 143 if node.flags & node.SECTION: 144 result += u'\t' * level + node.label + '\n' 145 for sub in node.value.values(): 146 result += print_node(sub, level + 1) 147 else: 148 if isinstance(node.value, (tuple, list)): 149 value = ', '.join(unicode(v) for v in node.value) 150 elif isinstance(node.value, float): 151 value = '%.2f' % node.value 152 else: 153 value = node.value 154 result += u'\t' * level + u'%-20s %s\n' % (node.label + ':', value) 155 return result 156 157 result = u'Nickname: %s\n' % self.name 158 if self.status & Contact.STATUS_ONLINE: 159 s = 'online' 160 elif self.status & Contact.STATUS_OFFLINE: 161 s = 'offline' 162 elif self.status & Contact.STATUS_AWAY: 163 s = 'away' 164 else: 165 s = 'unknown' 166 result += u'Status: %s (%s)\n' % (s, self.status_msg) 167 result += u'URL: %s\n' % self.url 168 result += u'Photos:\n' 169 for name, photo in self.photos.items(): 170 result += u'\t%s%s\n' % (photo, ' (hidden)' if photo.hidden else '') 171 result += u'\nProfile:\n' 172 for head in self.profile.values(): 173 result += print_node(head) 174 result += u'Description:\n' 175 for s in self.summary.split('\n'): 176 result += u'\t%s\n' % s 177 return result 178 179 180class QueryError(UserError): 181 """ 182 Raised when unable to send a query to a contact. 183 """ 184 185 186class Query(BaseObject): 187 """ 188 Query to send to a contact. 189 """ 190 message = StringField('Message received') 191 192 def __init__(self, id, message, url=None): 193 super(Query, self).__init__(id, url) 194 self.message = message 195 196 197class CapContact(Capability): 198 def iter_contacts(self, status=Contact.STATUS_ALL, ids=None): 199 """ 200 Iter contacts 201 202 :param status: get only contacts with the specified status 203 :type status: Contact.STATUS_* 204 :param ids: if set, get the specified contacts 205 :type ids: list[str] 206 :rtype: iter[:class:`Contact`] 207 """ 208 raise NotImplementedError() 209 210 def get_contact(self, id): 211 """ 212 Get a contact from his id. 213 214 The default implementation only calls iter_contacts() 215 with the proper values, but it might be overloaded 216 by backends. 217 218 :param id: the ID requested 219 :type id: str 220 :rtype: :class:`Contact` or None if not found 221 """ 222 223 l = self.iter_contacts(ids=[id]) 224 try: 225 return l[0] 226 except IndexError: 227 return None 228 229 def send_query(self, id): 230 """ 231 Send a query to a contact 232 233 :param id: the ID of contact 234 :type id: str 235 :rtype: :class:`Query` 236 :raises: :class:`QueryError` 237 """ 238 raise NotImplementedError() 239 240 def get_notes(self, id): 241 """ 242 Get personal notes about a contact 243 244 :param id: the ID of the contact 245 :type id: str 246 :rtype: unicode 247 """ 248 raise NotImplementedError() 249 250 def save_notes(self, id, notes): 251 """ 252 Set personal notes about a contact 253 254 :param id: the ID of the contact 255 :type id: str 256 :returns: the unicode object to save as notes 257 """ 258 raise NotImplementedError() 259 260 261class PhysicalEntity(BaseContact): 262 """ 263 Contact which has a physical address. 264 """ 265 postal_address = Field('Postal address', PostalAddress) 266 address_notes = StringField('Extra address info') 267 268 country = compat_field('postal_address', 'country') 269 postcode = compat_field('postal_address', 'postal_code') 270 city = compat_field('postal_address', 'city') 271 address = compat_field('postal_address', 'street') 272 273 274class Person(PhysicalEntity): 275 pass 276 277 278class OpeningHours(BaseObject): 279 """ 280 Definition of times when a place is open or closed. 281 282 Consists in a list of :class:`OpeningRule`. 283 Rules should be ordered by priority. 284 If no rule matches the given date, it is considered closed by default. 285 """ 286 287 rules = Field('Rules of opening/closing', list) 288 289 def is_open_at(self, query): 290 for rule in self.rules: 291 if query in rule: 292 return rule.is_open 293 294 return False 295 296 @property 297 def is_open_now(self): 298 return self.is_open_at(datetime.now()) 299 300 301class RRuleField(Field): 302 def __init__(self, doc, **kargs): 303 super(RRuleField, self).__init__(doc, rrule.rrulebase) 304 305 def convert(self, v): 306 if isinstance(v, basestring): 307 return rrule.rrulestr(v) 308 return v 309 310 311class OpeningRule(BaseObject): 312 """ 313 Single rule defining a (recurrent) time interval when a place is open or closed. 314 """ 315 dates = RRuleField('Dates on which this rule applies') 316 times = Field('Times of the day this rule applies', list) 317 is_open = BoolField('Is it an opening rule or closing rule?') 318 319 def __contains__(self, dt): 320 date = dt.date() 321 time = dt.time() 322 323 # check times before dates because there are probably fewer entries 324 for start, end in self.times: 325 if start <= time <= end: 326 break 327 else: 328 return False 329 330 # can't use "date in self.dates" because rrule only matches datetimes 331 for occ in self.dates: 332 occ = occ.date() 333 if occ > date: 334 break 335 elif occ == date: 336 return True 337 338 return False 339 340 341class Place(PhysicalEntity): 342 opening = Field('Opening hours', OpeningHours) 343 344 345class SearchQuery(BaseObject): 346 """ 347 Parameters to search for contacts. 348 """ 349 name = StringField('Name to search for') 350 location = Field('Address where to look', PostalAddress) 351 352 address = compat_field('location', 'street') 353 city = compat_field('location', 'city') 354 355 356class CapDirectory(Capability): 357 def search_contacts(self, query, sortby): 358 """ 359 Search contacts matching a query. 360 361 :param query: search parameters 362 :type query: :class:`SearchQuery` 363 :rtype: iter[:class:`PhysicalEntity`] 364 """ 365 raise NotImplementedError() 366