[MOD] New icons added
[odoo/odoo.git] / bin / tools / misc.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 """
24 Miscelleanous tools used by OpenERP.
25 """
26
27 import os, time, sys
28 import inspect
29
30 from config import config
31
32 import zipfile
33 import release
34 import socket
35 import re
36 from itertools import islice
37 import threading
38 from which import which
39
40
41 if sys.version_info[:2] < (2, 4):
42     from threadinglocal import local
43 else:
44     from threading import local
45
46 from itertools import izip
47
48 # initialize a database with base/base.sql
49 def init_db(cr):
50     import addons
51     f = addons.get_module_resource('base', 'base.sql')
52     for line in file_open(f).read().split(';'):
53         if (len(line)>0) and (not line.isspace()):
54             cr.execute(line)
55     cr.commit()
56
57     for i in addons.get_modules():
58         mod_path = addons.get_module_path(i)
59         if not mod_path:
60             continue
61
62         info = addons.load_information_from_description_file(i)
63
64         if not info:
65             continue
66         categs = info.get('category', 'Uncategorized').split('/')
67         p_id = None
68         while categs:
69             if p_id is not None:
70                 cr.execute('select id \
71                            from ir_module_category \
72                            where name=%s and parent_id=%s', (categs[0], p_id))
73             else:
74                 cr.execute('select id \
75                            from ir_module_category \
76                            where name=%s and parent_id is NULL', (categs[0],))
77             c_id = cr.fetchone()
78             if not c_id:
79                 cr.execute('select nextval(\'ir_module_category_id_seq\')')
80                 c_id = cr.fetchone()[0]
81                 cr.execute('insert into ir_module_category \
82                         (id, name, parent_id) \
83                         values (%s, %s, %s)', (c_id, categs[0], p_id))
84             else:
85                 c_id = c_id[0]
86             p_id = c_id
87             categs = categs[1:]
88
89         active = info.get('active', False)
90         installable = info.get('installable', True)
91         if installable:
92             if active:
93                 state = 'to install'
94             else:
95                 state = 'uninstalled'
96         else:
97             state = 'uninstallable'
98         cr.execute('select nextval(\'ir_module_module_id_seq\')')
99         id = cr.fetchone()[0]
100         cr.execute('insert into ir_module_module \
101                 (id, author, website, name, shortdesc, description, \
102                     category_id, state, certificate) \
103                 values (%s, %s, %s, %s, %s, %s, %s, %s, %s)', (
104             id, info.get('author', ''),
105             info.get('website', ''), i, info.get('name', False),
106             info.get('description', ''), p_id, state, info.get('certificate') or None))
107         cr.execute('insert into ir_model_data \
108             (name,model,module, res_id, noupdate) values (%s,%s,%s,%s,%s)', (
109                 'module_meta_information', 'ir.module.module', i, id, True))
110         dependencies = info.get('depends', [])
111         for d in dependencies:
112             cr.execute('insert into ir_module_module_dependency \
113                     (module_id,name) values (%s, %s)', (id, d))
114         cr.commit()
115
116 def find_in_path(name):
117     try:
118         return which(name)
119     except IOError:
120         return None
121
122 def find_pg_tool(name):
123     path = None
124     if config['pg_path'] and config['pg_path'] != 'None':
125         path = config['pg_path']
126     try:
127         return which(name, path=path)
128     except IOError:
129         return None
130
131 def exec_pg_command(name, *args):
132     prog = find_pg_tool(name)
133     if not prog:
134         raise Exception('Couldn\'t find %s' % name)
135     args2 = (os.path.basename(prog),) + args
136     return os.spawnv(os.P_WAIT, prog, args2)
137
138 def exec_pg_command_pipe(name, *args):
139     prog = find_pg_tool(name)
140     if not prog:
141         raise Exception('Couldn\'t find %s' % name)
142     if os.name == "nt":
143         cmd = '"' + prog + '" ' + ' '.join(args)
144     else:
145         cmd = prog + ' ' + ' '.join(args)
146     return os.popen2(cmd, 'b')
147
148 def exec_command_pipe(name, *args):
149     prog = find_in_path(name)
150     if not prog:
151         raise Exception('Couldn\'t find %s' % name)
152     if os.name == "nt":
153         cmd = '"'+prog+'" '+' '.join(args)
154     else:
155         cmd = prog+' '+' '.join(args)
156     return os.popen2(cmd, 'b')
157
158 #----------------------------------------------------------
159 # File paths
160 #----------------------------------------------------------
161 #file_path_root = os.getcwd()
162 #file_path_addons = os.path.join(file_path_root, 'addons')
163
164 def file_open(name, mode="r", subdir='addons', pathinfo=False):
165     """Open a file from the OpenERP root, using a subdir folder.
166
167     >>> file_open('hr/report/timesheer.xsl')
168     >>> file_open('addons/hr/report/timesheet.xsl')
169     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
170
171     @param name: name of the file
172     @param mode: file open mode
173     @param subdir: subdirectory
174     @param pathinfo: if True returns tupple (fileobject, filepath)
175
176     @return: fileobject if pathinfo is False else (fileobject, filepath)
177     """
178     import addons
179     adps = addons.ad_paths
180     rtp = os.path.normcase(os.path.abspath(config['root_path']))
181
182     if name.replace(os.path.sep, '/').startswith('addons/'):
183         subdir = 'addons'
184         name = name[7:]
185
186     # First try to locate in addons_path
187     if subdir:
188         subdir2 = subdir
189         if subdir2.replace(os.path.sep, '/').startswith('addons/'):
190             subdir2 = subdir2[7:]
191
192         subdir2 = (subdir2 != 'addons' or None) and subdir2
193
194         for adp in adps:
195           try:
196             if subdir2:
197                 fn = os.path.join(adp, subdir2, name)
198             else:
199                 fn = os.path.join(adp, name)
200             fn = os.path.normpath(fn)
201             fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
202             if pathinfo:
203                 return fo, fn
204             return fo
205           except IOError, e:
206             pass
207
208     if subdir:
209         name = os.path.join(rtp, subdir, name)
210     else:
211         name = os.path.join(rtp, name)
212
213     name = os.path.normpath(name)
214
215     # Check for a zipfile in the path
216     head = name
217     zipname = False
218     name2 = False
219     while True:
220         head, tail = os.path.split(head)
221         if not tail:
222             break
223         if zipname:
224             zipname = os.path.join(tail, zipname)
225         else:
226             zipname = tail
227         if zipfile.is_zipfile(head+'.zip'):
228             from cStringIO import StringIO
229             zfile = zipfile.ZipFile(head+'.zip')
230             try:
231                 fo = StringIO()
232                 fo.write(zfile.read(os.path.join(
233                     os.path.basename(head), zipname).replace(
234                         os.sep, '/')))
235                 fo.seek(0)
236                 if pathinfo:
237                     return fo, name
238                 return fo
239             except:
240                 name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
241                 pass
242     for i in (name2, name):
243         if i and os.path.isfile(i):
244             fo = file(i, mode)
245             if pathinfo:
246                 return fo, i
247             return fo
248     if os.path.splitext(name)[1] == '.rml':
249         raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
250     raise IOError, 'File not found : '+str(name)
251
252
253 #----------------------------------------------------------
254 # iterables
255 #----------------------------------------------------------
256 def flatten(list):
257     """Flatten a list of elements into a uniqu list
258     Author: Christophe Simonis (christophe@tinyerp.com)
259
260     Examples:
261     >>> flatten(['a'])
262     ['a']
263     >>> flatten('b')
264     ['b']
265     >>> flatten( [] )
266     []
267     >>> flatten( [[], [[]]] )
268     []
269     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
270     ['a', 'b', 'c', 'd', 'e', 'f']
271     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
272     >>> flatten(t)
273     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
274     """
275
276     def isiterable(x):
277         return hasattr(x, "__iter__")
278
279     r = []
280     for e in list:
281         if isiterable(e):
282             map(r.append, flatten(e))
283         else:
284             r.append(e)
285     return r
286
287 def reverse_enumerate(l):
288     """Like enumerate but in the other sens
289     >>> a = ['a', 'b', 'c']
290     >>> it = reverse_enumerate(a)
291     >>> it.next()
292     (2, 'c')
293     >>> it.next()
294     (1, 'b')
295     >>> it.next()
296     (0, 'a')
297     >>> it.next()
298     Traceback (most recent call last):
299       File "<stdin>", line 1, in <module>
300     StopIteration
301     """
302     return izip(xrange(len(l)-1, -1, -1), reversed(l))
303
304 #----------------------------------------------------------
305 # Emails
306 #----------------------------------------------------------
307 email_re = re.compile(r"""
308     ([a-zA-Z][\w\.-]*[a-zA-Z0-9]     # username part
309     @                                # mandatory @ sign
310     [a-zA-Z0-9][\w\.-]*              # domain must start with a letter ... Ged> why do we include a 0-9 then?
311      \.
312      [a-z]{2,3}                      # TLD
313     )
314     """, re.VERBOSE)
315 res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
316 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
317 reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
318
319 priorities = {
320         '1': '1 (Highest)',
321         '2': '2 (High)',
322         '3': '3 (Normal)',
323         '4': '4 (Low)',
324         '5': '5 (Lowest)',
325     }
326
327 def html2plaintext(html, body_id=None, encoding='utf-8'):
328     ## (c) Fry-IT, www.fry-it.com, 2007
329     ## <peter@fry-it.com>
330     ## download here: http://www.peterbe.com/plog/html2plaintext
331
332
333     """ from an HTML text, convert the HTML to plain text.
334     If @body_id is provided then this is the tag where the
335     body (not necessarily <body>) starts.
336     """
337     try:
338         from BeautifulSoup import BeautifulSoup, SoupStrainer, Comment
339     except:
340         return html
341
342     urls = []
343     if body_id is not None:
344         strainer = SoupStrainer(id=body_id)
345     else:
346         strainer = SoupStrainer('body')
347
348     soup = BeautifulSoup(html, parseOnlyThese=strainer, fromEncoding=encoding)
349     for link in soup.findAll('a'):
350         title = link.renderContents()
351         for url in [x[1] for x in link.attrs if x[0]=='href']:
352             urls.append(dict(url=url, tag=str(link), title=title))
353
354     html = soup.__str__()
355
356     url_index = []
357     i = 0
358     for d in urls:
359         if d['title'] == d['url'] or 'http://'+d['title'] == d['url']:
360             html = html.replace(d['tag'], d['url'])
361         else:
362             i += 1
363             html = html.replace(d['tag'], '%s [%s]' % (d['title'], i))
364             url_index.append(d['url'])
365
366     html = html.replace('<strong>','*').replace('</strong>','*')
367     html = html.replace('<b>','*').replace('</b>','*')
368     html = html.replace('<h3>','*').replace('</h3>','*')
369     html = html.replace('<h2>','**').replace('</h2>','**')
370     html = html.replace('<h1>','**').replace('</h1>','**')
371     html = html.replace('<em>','/').replace('</em>','/')
372
373
374     # the only line breaks we respect is those of ending tags and
375     # breaks
376
377     html = html.replace('\n',' ')
378     html = html.replace('<br>', '\n')
379     html = html.replace('<tr>', '\n')
380     html = html.replace('</p>', '\n\n')
381     html = re.sub('<br\s*/>', '\n', html)
382     html = html.replace(' ' * 2, ' ')
383
384
385     # for all other tags we failed to clean up, just remove then and
386     # complain about them on the stderr
387     def desperate_fixer(g):
388         #print >>sys.stderr, "failed to clean up %s" % str(g.group())
389         return ' '
390
391     html = re.sub('<.*?>', desperate_fixer, html)
392
393     # lstrip all lines
394     html = '\n'.join([x.lstrip() for x in html.splitlines()])
395
396     for i, url in enumerate(url_index):
397         if i == 0:
398             html += '\n\n'
399         html += '[%s] %s\n' % (i+1, url)
400     return html
401
402 def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
403                attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
404
405     """Send an email.
406
407     Arguments:
408
409     `email_from`: A string used to fill the `From` header, if falsy,
410                   config['email_from'] is used instead.  Also used for
411                   the `Reply-To` header if `reply_to` is not provided
412
413     `email_to`: a sequence of addresses to send the mail to.
414     """
415     import smtplib
416     from email.MIMEText import MIMEText
417     from email.MIMEBase import MIMEBase
418     from email.MIMEMultipart import MIMEMultipart
419     from email.Header import Header
420     from email.Utils import formatdate, COMMASPACE
421     from email.Utils import formatdate, COMMASPACE
422     from email import Encoders
423     import netsvc
424
425     if x_headers is None:
426         x_headers = {}
427
428     if not ssl: ssl = config.get('smtp_ssl', False)
429
430     if not (email_from or config['email_from']):
431         raise ValueError("Sending an email requires either providing a sender "
432                          "address or having configured one")
433
434     if not email_from: email_from = config.get('email_from', False)
435
436     if not email_cc: email_cc = []
437     if not email_bcc: email_bcc = []
438     if not body: body = u''
439     try: email_body = body.encode('utf-8')
440     except (UnicodeEncodeError, UnicodeDecodeError):
441         email_body = body
442
443     try:
444         email_text = MIMEText(email_body.encode('utf8') or '',_subtype=subtype,_charset='utf-8')
445     except:
446         email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
447
448     if attach: msg = MIMEMultipart()
449     else: msg = email_text
450
451     msg['Subject'] = Header(ustr(subject), 'utf-8')
452     msg['From'] = email_from
453     del msg['Reply-To']
454     if reply_to:
455         msg['Reply-To'] = reply_to
456     else:
457         msg['Reply-To'] = msg['From']
458     msg['To'] = COMMASPACE.join(email_to)
459     if email_cc:
460         msg['Cc'] = COMMASPACE.join(email_cc)
461     if email_bcc:
462         msg['Bcc'] = COMMASPACE.join(email_bcc)
463     msg['Date'] = formatdate(localtime=True)
464
465     # Add OpenERP Server information
466     msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)'
467     msg['X-OpenERP-Server-Host'] = socket.gethostname()
468     msg['X-OpenERP-Server-Version'] = release.version
469     msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
470
471     # Add dynamic X Header
472     for key, value in x_headers.iteritems():
473         msg['X-OpenERP-%s' % key] = str(value)
474         msg['%s' % key] = str(value)
475
476     if openobject_id:
477         msg['Message-Id'] = "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
478
479     if attach:
480         msg.attach(email_text)
481         for (fname,fcontent) in attach:
482             part = MIMEBase('application', "octet-stream")
483             part.set_payload( fcontent )
484             Encoders.encode_base64(part)
485             part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
486             msg.attach(part)
487
488     class WriteToLogger(object):
489         def __init__(self):
490             self.logger = netsvc.Logger()
491
492         def write(self, s):
493             self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
494
495     smtp_server = config['smtp_server']
496     if smtp_server.startswith('maildir:/'):
497         from mailbox import Maildir
498         maildir_path = smtp_server[8:]
499         try:
500             mdir = Maildir(maildir_path,factory=None, create = True)
501             mdir.add(msg.as_string(True))
502             return True
503         except Exception,e:
504             netsvc.Logger().notifyChannel('email_send (maildir)', netsvc.LOG_ERROR, e)
505             return False
506
507     try:
508         oldstderr = smtplib.stderr
509         s = smtplib.SMTP()
510         try:
511             # in case of debug, the messages are printed to stderr.
512             if debug:
513                 smtplib.stderr = WriteToLogger()
514
515             s.set_debuglevel(int(bool(debug)))  # 0 or 1
516             s.connect(smtp_server, config['smtp_port'])
517             if ssl:
518                 s.ehlo()
519                 s.starttls()
520                 s.ehlo()
521
522             if config['smtp_user'] or config['smtp_password']:
523                 s.login(config['smtp_user'], config['smtp_password'])
524             s.sendmail(email_from,
525                        flatten([email_to, email_cc, email_bcc]),
526                        msg.as_string()
527                       )
528         finally:
529             s.quit()
530             if debug:
531                 smtplib.stderr = oldstderr
532
533     except Exception, e:
534         netsvc.Logger().notifyChannel('email_send', netsvc.LOG_ERROR, e)
535         return False
536
537     return True
538
539 #----------------------------------------------------------
540 # SMS
541 #----------------------------------------------------------
542 # text must be latin-1 encoded
543 def sms_send(user, password, api_id, text, to):
544     import urllib
545     url = "http://api.urlsms.com/SendSMS.aspx"
546     #url = "http://196.7.150.220/http/sendmsg"
547     params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
548     f = urllib.urlopen(url+"?"+params)
549     # FIXME: Use the logger if there is an error
550     return True
551
552 #---------------------------------------------------------
553 # Class that stores an updateable string (used in wizards)
554 #---------------------------------------------------------
555 class UpdateableStr(local):
556
557     def __init__(self, string=''):
558         self.string = string
559
560     def __str__(self):
561         return str(self.string)
562
563     def __repr__(self):
564         return str(self.string)
565
566     def __nonzero__(self):
567         return bool(self.string)
568
569
570 class UpdateableDict(local):
571     '''Stores an updateable dict to use in wizards'''
572
573     def __init__(self, dict=None):
574         if dict is None:
575             dict = {}
576         self.dict = dict
577
578     def __str__(self):
579         return str(self.dict)
580
581     def __repr__(self):
582         return str(self.dict)
583
584     def clear(self):
585         return self.dict.clear()
586
587     def keys(self):
588         return self.dict.keys()
589
590     def __setitem__(self, i, y):
591         self.dict.__setitem__(i, y)
592
593     def __getitem__(self, i):
594         return self.dict.__getitem__(i)
595
596     def copy(self):
597         return self.dict.copy()
598
599     def iteritems(self):
600         return self.dict.iteritems()
601
602     def iterkeys(self):
603         return self.dict.iterkeys()
604
605     def itervalues(self):
606         return self.dict.itervalues()
607
608     def pop(self, k, d=None):
609         return self.dict.pop(k, d)
610
611     def popitem(self):
612         return self.dict.popitem()
613
614     def setdefault(self, k, d=None):
615         return self.dict.setdefault(k, d)
616
617     def update(self, E, **F):
618         return self.dict.update(E, F)
619
620     def values(self):
621         return self.dict.values()
622
623     def get(self, k, d=None):
624         return self.dict.get(k, d)
625
626     def has_key(self, k):
627         return self.dict.has_key(k)
628
629     def items(self):
630         return self.dict.items()
631
632     def __cmp__(self, y):
633         return self.dict.__cmp__(y)
634
635     def __contains__(self, k):
636         return self.dict.__contains__(k)
637
638     def __delitem__(self, y):
639         return self.dict.__delitem__(y)
640
641     def __eq__(self, y):
642         return self.dict.__eq__(y)
643
644     def __ge__(self, y):
645         return self.dict.__ge__(y)
646
647     def __gt__(self, y):
648         return self.dict.__gt__(y)
649
650     def __hash__(self):
651         return self.dict.__hash__()
652
653     def __iter__(self):
654         return self.dict.__iter__()
655
656     def __le__(self, y):
657         return self.dict.__le__(y)
658
659     def __len__(self):
660         return self.dict.__len__()
661
662     def __lt__(self, y):
663         return self.dict.__lt__(y)
664
665     def __ne__(self, y):
666         return self.dict.__ne__(y)
667
668
669 # Don't use ! Use res.currency.round()
670 class currency(float):
671
672     def __init__(self, value, accuracy=2, rounding=None):
673         if rounding is None:
674             rounding=10**-accuracy
675         self.rounding=rounding
676         self.accuracy=accuracy
677
678     def __new__(cls, value, accuracy=2, rounding=None):
679         return float.__new__(cls, round(value, accuracy))
680
681     #def __str__(self):
682     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
683     #   return str(display_value)
684
685
686 def is_hashable(h):
687     try:
688         hash(h)
689         return True
690     except TypeError:
691         return False
692
693 class cache(object):
694     """
695     Use it as a decorator of the function you plan to cache
696     Timeout: 0 = no timeout, otherwise in seconds
697     """
698
699     __caches = []
700
701     def __init__(self, timeout=None, skiparg=2, multi=None):
702         assert skiparg >= 2 # at least self and cr
703         if timeout is None:
704             self.timeout = config['cache_timeout']
705         else:
706             self.timeout = timeout
707         self.skiparg = skiparg
708         self.multi = multi
709         self.lasttime = time.time()
710         self.cache = {}
711         self.fun = None
712         cache.__caches.append(self)
713
714
715     def _generate_keys(self, dbname, kwargs2):
716         """
717         Generate keys depending of the arguments and the self.mutli value
718         """
719
720         def to_tuple(d):
721             pairs = d.items()
722             pairs.sort(key=lambda (k,v): k)
723             for i, (k, v) in enumerate(pairs):
724                 if isinstance(v, dict):
725                     pairs[i] = (k, to_tuple(v))
726                 if isinstance(v, (list, set)):
727                     pairs[i] = (k, tuple(v))
728                 elif not is_hashable(v):
729                     pairs[i] = (k, repr(v))
730             return tuple(pairs)
731
732         if not self.multi:
733             key = (('dbname', dbname),) + to_tuple(kwargs2)
734             yield key, None
735         else:
736             multis = kwargs2[self.multi][:]
737             for id in multis:
738                 kwargs2[self.multi] = (id,)
739                 key = (('dbname', dbname),) + to_tuple(kwargs2)
740                 yield key, id
741
742     def _unify_args(self, *args, **kwargs):
743         # Update named arguments with positional argument values (without self and cr)
744         kwargs2 = self.fun_default_values.copy()
745         kwargs2.update(kwargs)
746         kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
747         return kwargs2
748
749     def clear(self, dbname, *args, **kwargs):
750         """clear the cache for database dbname
751             if *args and **kwargs are both empty, clear all the keys related to this database
752         """
753         if not args and not kwargs:
754             keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname]
755         else:
756             kwargs2 = self._unify_args(*args, **kwargs)
757             keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()]
758
759         for key in keys_to_del:
760             self.cache.pop(key)
761
762     @classmethod
763     def clean_caches_for_db(cls, dbname):
764         for c in cls.__caches:
765             c.clear(dbname)
766
767     def __call__(self, fn):
768         if self.fun is not None:
769             raise Exception("Can not use a cache instance on more than one function")
770         self.fun = fn
771
772         argspec = inspect.getargspec(fn)
773         self.fun_arg_names = argspec[0][self.skiparg:]
774         self.fun_default_values = {}
775         if argspec[3]:
776             self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
777
778         def cached_result(self2, cr, *args, **kwargs):
779             if time.time()-int(self.timeout) > self.lasttime:
780                 self.lasttime = time.time()
781                 t = time.time()-int(self.timeout)
782                 old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t]
783                 for key in old_keys:
784                     self.cache.pop(key)
785
786             kwargs2 = self._unify_args(*args, **kwargs)
787
788             result = {}
789             notincache = {}
790             for key, id in self._generate_keys(cr.dbname, kwargs2):
791                 if key in self.cache:
792                     result[id] = self.cache[key][0]
793                 else:
794                     notincache[id] = key
795
796             if notincache:
797                 if self.multi:
798                     kwargs2[self.multi] = notincache.keys()
799
800                 result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2)
801                 if not self.multi:
802                     key = notincache[None]
803                     self.cache[key] = (result2, time.time())
804                     result[None] = result2
805                 else:
806                     for id in result2:
807                         key = notincache[id]
808                         self.cache[key] = (result2[id], time.time())
809                     result.update(result2)
810
811             if not self.multi:
812                 return result[None]
813             return result
814
815         cached_result.clear_cache = self.clear
816         return cached_result
817
818 def to_xml(s):
819     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
820
821 def get_encodings():
822     yield 'utf8'
823     from locale import getpreferredencoding
824     prefenc = getpreferredencoding()
825     if prefenc:
826         yield prefenc
827
828         prefenc = {
829             'latin1': 'latin9',
830             'iso-8859-1': 'iso8859-15',
831             'cp1252': '1252',
832         }.get(prefenc.lower())
833         if prefenc:
834             yield prefenc
835
836
837 def ustr(value):
838     """This method is similar to the builtin `str` method, except
839     it will return Unicode string.
840
841     @param value: the value to convert
842
843     @rtype: unicode
844     @return: unicode string
845     """
846     orig = value
847     if isinstance(value, Exception):
848         return exception_to_unicode(value)
849
850     if isinstance(value, unicode):
851         return value
852
853     try:
854         return unicode(value)
855     except:
856         pass
857
858     for ln in get_encodings():
859         try:
860             return unicode(value, ln)
861         except:
862             pass
863     raise UnicodeError('unable de to convert %r' % (orig,))
864
865
866 def exception_to_unicode(e):
867     if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
868         return ustr(e.message)
869     if hasattr(e, 'args'):
870         return "\n".join((ustr(a) for a in e.args))
871     try:
872         return ustr(e)
873     except:
874         return u"Unknown message"
875
876
877 # to be compatible with python 2.4
878 import __builtin__
879 if not hasattr(__builtin__, 'all'):
880     def all(iterable):
881         for element in iterable:
882             if not element:
883                 return False
884         return True
885
886     __builtin__.all = all
887     del all
888
889 if not hasattr(__builtin__, 'any'):
890     def any(iterable):
891         for element in iterable:
892             if element:
893                 return True
894         return False
895
896     __builtin__.any = any
897     del any
898
899 def get_iso_codes(lang):
900     if lang.find('_') != -1:
901         if lang.split('_')[0] == lang.split('_')[1].lower():
902             lang = lang.split('_')[0]
903     return lang
904
905 def get_languages():
906     languages={
907         'ab_RU': u'Abkhazian (RU)',
908         'ar_AR': u'Arabic / الْعَرَبيّة',
909         'bg_BG': u'Bulgarian / български',
910         'bs_BS': u'Bosnian / bosanski jezik',
911         'ca_ES': u'Catalan / Català',
912         'cs_CZ': u'Czech / Čeština',
913         'da_DK': u'Danish / Dansk',
914         'de_DE': u'German / Deutsch',
915         'el_GR': u'Greek / Ελληνικά',
916         'en_CA': u'English (CA)',
917         'en_GB': u'English (UK)',
918         'en_US': u'English (US)',
919         'es_AR': u'Spanish (AR) / Español (AR)',
920         'es_ES': u'Spanish / Español',
921         'et_EE': u'Estonian / Eesti keel',
922         'fa_IR': u'Persian / فارس',
923         'fi_FI': u'Finland / Suomi',
924         'fr_BE': u'French (BE) / Français (BE)',
925         'fr_CH': u'French (CH) / Français (CH)',
926         'fr_FR': u'French / Français',
927         'gl_ES': u'Galician / Galego',
928         'gu_IN': u'Gujarati / India',
929         'hi_IN': u'Hindi / India',
930         'hr_HR': u'Croatian / hrvatski jezik',
931         'hu_HU': u'Hungarian / Magyar',
932         'id_ID': u'Indonesian / Bahasa Indonesia',
933         'it_IT': u'Italian / Italiano',
934         'iu_CA': u'Inuktitut / Canada',
935         'ja_JP': u'Japanese / Japan',
936         'ko_KP': u'Korean / Korea, Democratic Peoples Republic of',
937         'ko_KR': u'Korean / Korea, Republic of',
938         'lt_LT': u'Lithuanian / Lietuvių kalba',
939         'lv_LV': u'Latvian / Latvia',
940         'ml_IN': u'Malayalam / India',
941         'mn_MN': u'Mongolian / Mongolia',
942         'nb_NO': u'Norwegian Bokmål / Norway',
943         'nl_NL': u'Dutch / Nederlands',
944         'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
945         'oc_FR': u'Occitan (post 1500) / France',
946         'pl_PL': u'Polish / Język polski',
947         'pt_BR': u'Portugese (BR) / português (BR)',
948         'pt_PT': u'Portugese / português',
949         'ro_RO': u'Romanian / limba română',
950         'ru_RU': u'Russian / русский язык',
951         'si_LK': u'Sinhalese / Sri Lanka',
952         'sl_SI': u'Slovenian / slovenščina',
953         'sk_SK': u'Slovak / Slovenský jazyk',
954         'sq_AL': u'Albanian / Shqipëri',
955         'sr_RS': u'Serbian / Serbia',
956         'sv_SE': u'Swedish / svenska',
957         'te_IN': u'Telugu / India',
958         'tr_TR': u'Turkish / Türkçe',
959         'vi_VN': u'Vietnam / Cộng hòa xã hội chủ nghĩa Việt Nam',
960         'uk_UA': u'Ukrainian / украї́нська мо́ва',
961         'ur_PK': u'Urdu / Pakistan',
962         'zh_CN': u'Chinese (CN) / 简体中文',
963         'zh_HK': u'Chinese (HK)',
964         'zh_TW': u'Chinese (TW) / 正體字',
965         'th_TH': u'Thai / ภาษาไทย',
966         'tlh_TLH': u'Klingon',
967     }
968     return languages
969
970 def scan_languages():
971 #    import glob
972 #    file_list = [os.path.splitext(os.path.basename(f))[0] for f in glob.glob(os.path.join(config['root_path'],'addons', 'base', 'i18n', '*.po'))]
973 #    ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
974     # Now it will take all languages from get languages function without filter it with base module languages
975     lang_dict = get_languages()
976     ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
977     ret.sort(key=lambda k:k[1])
978     return ret
979
980
981 def get_user_companies(cr, user):
982     def _get_company_children(cr, ids):
983         if not ids:
984             return []
985         cr.execute('SELECT id FROM res_company WHERE parent_id = ANY (%s)', (ids,))
986         res=[x[0] for x in cr.fetchall()]
987         res.extend(_get_company_children(cr, res))
988         return res
989     cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %s AND comp.id = u.company_id', (user,))
990     compids=[cr.fetchone()[0]]
991     compids.extend(_get_company_children(cr, compids))
992     return compids
993
994 def mod10r(number):
995     """
996     Input number : account or invoice number
997     Output return: the same number completed with the recursive mod10
998     key
999     """
1000     codec=[0,9,4,6,8,2,7,1,3,5]
1001     report = 0
1002     result=""
1003     for digit in number:
1004         result += digit
1005         if digit.isdigit():
1006             report = codec[ (int(digit) + report) % 10 ]
1007     return result + str((10 - report) % 10)
1008
1009
1010 def human_size(sz):
1011     """
1012     Return the size in a human readable format
1013     """
1014     if not sz:
1015         return False
1016     units = ('bytes', 'Kb', 'Mb', 'Gb')
1017     if isinstance(sz,basestring):
1018         sz=len(sz)
1019     s, i = float(sz), 0
1020     while s >= 1024 and i < len(units)-1:
1021         s = s / 1024
1022         i = i + 1
1023     return "%0.2f %s" % (s, units[i])
1024
1025 def logged(f):
1026     from tools.func import wraps
1027
1028     @wraps(f)
1029     def wrapper(*args, **kwargs):
1030         import netsvc
1031         from pprint import pformat
1032
1033         vector = ['Call -> function: %r' % f]
1034         for i, arg in enumerate(args):
1035             vector.append('  arg %02d: %s' % (i, pformat(arg)))
1036         for key, value in kwargs.items():
1037             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
1038
1039         timeb4 = time.time()
1040         res = f(*args, **kwargs)
1041
1042         vector.append('  result: %s' % pformat(res))
1043         vector.append('  time delta: %s' % (time.time() - timeb4))
1044         netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
1045         return res
1046
1047     return wrapper
1048
1049 class profile(object):
1050     def __init__(self, fname=None):
1051         self.fname = fname
1052
1053     def __call__(self, f):
1054         from tools.func import wraps
1055
1056         @wraps(f)
1057         def wrapper(*args, **kwargs):
1058             class profile_wrapper(object):
1059                 def __init__(self):
1060                     self.result = None
1061                 def __call__(self):
1062                     self.result = f(*args, **kwargs)
1063             pw = profile_wrapper()
1064             import cProfile
1065             fname = self.fname or ("%s.cprof" % (f.func_name,))
1066             cProfile.runctx('pw()', globals(), locals(), filename=fname)
1067             return pw.result
1068
1069         return wrapper
1070
1071 def debug(what):
1072     """
1073         This method allow you to debug your code without print
1074         Example:
1075         >>> def func_foo(bar)
1076         ...     baz = bar
1077         ...     debug(baz)
1078         ...     qnx = (baz, bar)
1079         ...     debug(qnx)
1080         ...
1081         >>> func_foo(42)
1082
1083         This will output on the logger:
1084
1085             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
1086             [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
1087
1088         To view the DEBUG lines in the logger you must start the server with the option
1089             --log-level=debug
1090
1091     """
1092     import netsvc
1093     from inspect import stack
1094     import re
1095     from pprint import pformat
1096     st = stack()[1]
1097     param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
1098     while param.count(')') > param.count('('): param = param[:param.rfind(')')]
1099     what = pformat(what)
1100     if param != what:
1101         what = "%s = %s" % (param, what)
1102     netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
1103
1104
1105 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
1106 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
1107 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
1108 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
1109 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
1110 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
1111 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
1112 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
1113 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
1114 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
1115 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
1116 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
1117 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
1118 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
1119 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
1120 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
1121 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
1122 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
1123 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
1124 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
1125 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
1126 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
1127 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
1128 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
1129 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
1130 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
1131 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
1132 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
1133 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
1134 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
1135 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
1136 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
1137 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
1138 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
1139 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
1140 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
1141 ])
1142
1143 def extract_zip_file(zip_file, outdirectory):
1144     import zipfile
1145     import os
1146
1147     zf = zipfile.ZipFile(zip_file, 'r')
1148     out = outdirectory
1149     for path in zf.namelist():
1150         tgt = os.path.join(out, path)
1151         tgtdir = os.path.dirname(tgt)
1152         if not os.path.exists(tgtdir):
1153             os.makedirs(tgtdir)
1154
1155         if not tgt.endswith(os.sep):
1156             fp = open(tgt, 'wb')
1157             fp.write(zf.read(path))
1158             fp.close()
1159     zf.close()
1160
1161 def detect_ip_addr():
1162     def _detect_ip_addr():
1163         from array import array
1164         import socket
1165         from struct import pack, unpack
1166
1167         try:
1168             import fcntl
1169         except ImportError:
1170             fcntl = None
1171
1172         ip_addr = None
1173
1174         if not fcntl: # not UNIX:
1175             host = socket.gethostname()
1176             ip_addr = socket.gethostbyname(host)
1177         else: # UNIX:
1178             # get all interfaces:
1179             nbytes = 128 * 32
1180             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1181             names = array('B', '\0' * nbytes)
1182             #print 'names: ', names
1183             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
1184             namestr = names.tostring()
1185
1186             # try 64 bit kernel:
1187             for i in range(0, outbytes, 40):
1188                 name = namestr[i:i+16].split('\0', 1)[0]
1189                 if name != 'lo':
1190                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
1191                     break
1192
1193             # try 32 bit kernel:
1194             if ip_addr is None:
1195                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
1196
1197                 for ifname in [iface for iface in ifaces if iface != 'lo']:
1198                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
1199                     break
1200
1201         return ip_addr or 'localhost'
1202
1203     try:
1204         ip_addr = _detect_ip_addr()
1205     except:
1206         ip_addr = 'localhost'
1207     return ip_addr
1208
1209 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
1210 #  The server side never does any timestamp calculation, always
1211 #  sends them in a naive (timezone agnostic) format supposed to be
1212 #  expressed within the server timezone, and expects the clients to
1213 #  provide timestamps in the server timezone as well.
1214 #  It stores all timestamps in the database in naive format as well,
1215 #  which also expresses the time in the server timezone.
1216 #  For this reason the server makes its timezone name available via the
1217 #  common/timezone_get() rpc method, which clients need to read
1218 #  to know the appropriate time offset to use when reading/writing
1219 #  times.
1220 def get_win32_timezone():
1221     """Attempt to return the "standard name" of the current timezone on a win32 system.
1222        @return: the standard name of the current win32 timezone, or False if it cannot be found.
1223     """
1224     res = False
1225     if (sys.platform == "win32"):
1226         try:
1227             import _winreg
1228             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
1229             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
1230             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
1231             _winreg.CloseKey(current_tz_key)
1232             _winreg.CloseKey(hklm)
1233         except:
1234             pass
1235     return res
1236
1237 def detect_server_timezone():
1238     """Attempt to detect the timezone to use on the server side.
1239        Defaults to UTC if no working timezone can be found.
1240        @return: the timezone identifier as expected by pytz.timezone.
1241     """
1242     import time
1243     import netsvc
1244     try:
1245         import pytz
1246     except:
1247         netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1248             "Python pytz module is not available. Timezone will be set to UTC by default.")
1249         return 'UTC'
1250
1251     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
1252     # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz
1253     # Option 3: the environment variable TZ
1254     sources = [ (config['timezone'], 'OpenERP configuration'),
1255                 (time.tzname[0], 'time.tzname'),
1256                 (os.environ.get('TZ',False),'TZ environment variable'), ]
1257     # Option 4: OS-specific: /etc/timezone on Unix
1258     if (os.path.exists("/etc/timezone")):
1259         tz_value = False
1260         try:
1261             f = open("/etc/timezone")
1262             tz_value = f.read(128).strip()
1263         except:
1264             pass
1265         finally:
1266             f.close()
1267         sources.append((tz_value,"/etc/timezone file"))
1268     # Option 5: timezone info from registry on Win32
1269     if (sys.platform == "win32"):
1270         # Timezone info is stored in windows registry.
1271         # However this is not likely to work very well as the standard name
1272         # of timezones in windows is rarely something that is known to pytz.
1273         # But that's ok, it is always possible to use a config option to set
1274         # it explicitly.
1275         sources.append((get_win32_timezone(),"Windows Registry"))
1276
1277     for (value,source) in sources:
1278         if value:
1279             try:
1280                 tz = pytz.timezone(value)
1281                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_INFO,
1282                     "Using timezone %s obtained from %s." % (tz.zone,source))
1283                 return value
1284             except pytz.UnknownTimeZoneError:
1285                 netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1286                     "The timezone specified in %s (%s) is invalid, ignoring it." % (source,value))
1287
1288     netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
1289         "No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
1290     return 'UTC'
1291
1292
1293 def split_every(n, iterable, piece_maker=tuple):
1294     """Splits an iterable into length-n pieces. The last piece will be shorter
1295        if ``n`` does not evenly divide the iterable length.
1296        @param ``piece_maker``: function to build the pieces
1297        from the slices (tuple,list,...)
1298     """
1299     iterator = iter(iterable)
1300     piece = piece_maker(islice(iterator, n))
1301     while piece:
1302         yield piece
1303         piece = piece_maker(islice(iterator, n))
1304
1305 if __name__ == '__main__':
1306     import doctest
1307     doctest.testmod()
1308
1309 class upload_data_thread(threading.Thread):
1310     def __init__(self, email, data, type):
1311         self.args = [('email',email),('type',type),('data',data)]
1312         super(upload_data_thread,self).__init__()
1313     def run(self):
1314         try:
1315             import urllib
1316             args = urllib.urlencode(self.args)
1317             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1318             fp.read()
1319             fp.close()
1320         except:
1321             pass
1322
1323 def upload_data(email, data, type='SURVEY'):
1324     a = upload_data_thread(email, data, type)
1325     a.start()
1326     return True
1327 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1328