[FIX] Set a default value if the x_headers argument has the value 'None'
[odoo/odoo.git] / bin / tools / misc.py
index fc89a45..1ead9ea 100644 (file)
@@ -2,7 +2,7 @@
 ##############################################################################
 #
 #    OpenERP, Open Source Management Solution
-#    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
+#    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
 #    $Id$
 #
 #    This program is free software: you can redistribute it and/or modify
@@ -27,10 +27,7 @@ Miscelleanous tools used by OpenERP.
 import os, time, sys
 import inspect
 
-import psycopg
-#import netsvc
 from config import config
-#import tools
 
 import zipfile
 import release
@@ -47,7 +44,7 @@ from itertools import izip
 def init_db(cr):
     import addons
     f = addons.get_module_resource('base', 'base.sql')
-    for line in file(f).read().split(';'):
+    for line in file_open(f).read().split(';'):
         if (len(line)>0) and (not line.isspace()):
             cr.execute(line)
     cr.commit()
@@ -58,12 +55,8 @@ def init_db(cr):
         if not mod_path:
             continue
         info = False
-        if os.path.isfile(terp_file) and not os.path.isfile(mod_path+'.zip'):
-            info = eval(file(terp_file).read())
-        elif zipfile.is_zipfile(mod_path+'.zip'):
-            zfile = zipfile.ZipFile(mod_path+'.zip')
-            i = os.path.splitext(i)[0]
-            info = eval(zfile.read(os.path.join(i, '__terp__.py')))
+        if os.path.isfile(terp_file) or os.path.isfile(mod_path+'.zip'):
+            info = eval(file_open(terp_file).read())
         if info:
             categs = info.get('category', 'Uncategorized').split('/')
             p_id = None
@@ -71,7 +64,7 @@ def init_db(cr):
                 if p_id is not None:
                     cr.execute('select id \
                             from ir_module_category \
-                            where name=%s and parent_id=%d', (categs[0], p_id))
+                            where name=%s and parent_id=%s', (categs[0], p_id))
                 else:
                     cr.execute('select id \
                             from ir_module_category \
@@ -82,7 +75,7 @@ def init_db(cr):
                     c_id = cr.fetchone()[0]
                     cr.execute('insert into ir_module_category \
                             (id, name, parent_id) \
-                            values (%d, %s, %d)', (c_id, categs[0], p_id))
+                            values (%s, %s, %s)', (c_id, categs[0], p_id))
                 else:
                     c_id = c_id[0]
                 p_id = c_id
@@ -100,13 +93,15 @@ def init_db(cr):
             cr.execute('select nextval(\'ir_module_module_id_seq\')')
             id = cr.fetchone()[0]
             cr.execute('insert into ir_module_module \
-                    (id, author, latest_version, website, name, shortdesc, description, \
+                    (id, author, website, name, shortdesc, description, \
                         category_id, state) \
-                    values (%d, %s, %s, %s, %s, %s, %s, %d, %s)', (
+                    values (%s, %s, %s, %s, %s, %s, %s, %s)', (
                 id, info.get('author', ''),
-                release.version.rsplit('.', 1)[0] + '.' + info.get('version', ''),
                 info.get('website', ''), i, info.get('name', False),
                 info.get('description', ''), p_id, state))
+            cr.execute('insert into ir_model_data \
+                (name,model,module, res_id) values (%s,%s,%s,%s)', (
+                    'module_meta_information', 'ir.module.module', i, id))
             dependencies = info.get('depends', [])
             for d in dependencies:
                 cr.execute('insert into ir_module_module_dependency \
@@ -228,13 +223,14 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
         else:
             zipname = tail
         if zipfile.is_zipfile(head+'.zip'):
-            import StringIO
+            from cStringIO import StringIO
             zfile = zipfile.ZipFile(head+'.zip')
             try:
-                fo = StringIO.StringIO(zfile.read(os.path.join(
+                fo = StringIO()
+                fo.write(zfile.read(os.path.join(
                     os.path.basename(head), zipname).replace(
                         os.sep, '/')))
-
+                fo.seek(0)
                 if pathinfo:
                     return fo, name
                 return fo
@@ -252,23 +248,6 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
     raise IOError, 'File not found : '+str(name)
 
 
-def oswalksymlinks(top, topdown=True, onerror=None):
-    """
-    same as os.walk but follow symlinks
-    attention: all symlinks are walked before all normals directories
-    """
-    for dirpath, dirnames, filenames in os.walk(top, topdown, onerror):
-        if topdown:
-            yield dirpath, dirnames, filenames
-
-        symlinks = filter(lambda dirname: os.path.islink(os.path.join(dirpath, dirname)), dirnames)
-        for s in symlinks:
-            for x in oswalksymlinks(os.path.join(dirpath, s), topdown, onerror):
-                yield x
-
-        if not topdown:
-            yield dirpath, dirnames, filenames
-
 #----------------------------------------------------------
 # iterables
 #----------------------------------------------------------
@@ -323,97 +302,72 @@ def reverse_enumerate(l):
 #----------------------------------------------------------
 # Emails
 #----------------------------------------------------------
-def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, tinycrm=False, ssl=False, debug=False,subtype='plain'):
+def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
+               attach=None, tinycrm=False, ssl=False, debug=False, subtype='plain', x_headers=None):
     """Send an email."""
-    if not email_cc:
-        email_cc=[]
-    if not email_bcc:
-        email_bcc=[]
     import smtplib
     from email.MIMEText import MIMEText
+    from email.MIMEBase import MIMEBase
     from email.MIMEMultipart import MIMEMultipart
     from email.Header import Header
     from email.Utils import formatdate, COMMASPACE
+    from email.Utils import formatdate, COMMASPACE
+    from email import Encoders
 
-    msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
-    msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
-    msg['From'] = email_from
-    del msg['Reply-To']
-    if reply_to:
-        msg['Reply-To'] = msg['From']+', '+reply_to
-    msg['To'] = COMMASPACE.join(email_to)
-    if email_cc:
-        msg['Cc'] = COMMASPACE.join(email_cc)
-    if email_bcc:
-        msg['Bcc'] = COMMASPACE.join(email_bcc)
-    msg['Date'] = formatdate(localtime=True)
-    if tinycrm:
-        msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
-    try:
-        s = smtplib.SMTP()
-
-        if debug:
-            s.debuglevel = 5
-        s.connect(config['smtp_server'], config['smtp_port'])
-        if ssl:
-            s.ehlo()
-            s.starttls()
-            s.ehlo()
+    if x_headers is None:
+        x_headers = {}
 
-        if config['smtp_user'] or config['smtp_password']:
-            s.login(config['smtp_user'], config['smtp_password'])
-        s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
-        s.quit()
-    except Exception, e:
-        import logging
-        logging.getLogger().error(str(e))
-    return True
+    if not ssl:
+        ssl = config.get('smtp_ssl', False)
 
+    if not email_from and not config['email_from']:
+        raise Exception("No Email sender by default, see config file")
 
-#----------------------------------------------------------
-# Emails
-#----------------------------------------------------------
-def email_send_attach(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, attach=None, tinycrm=False, ssl=False, debug=False):
-    """Send an email."""
     if not email_cc:
-        email_cc=[]
+        email_cc = []
     if not email_bcc:
-        email_bcc=[]
-    if not attach:
-        attach=[]
-    import smtplib
-    from email.MIMEText import MIMEText
-    from email.MIMEBase import MIMEBase
-    from email.MIMEMultipart import MIMEMultipart
-    from email.Header import Header
-    from email.Utils import formatdate, COMMASPACE
-    from email import Encoders
-
-    msg = MIMEMultipart()
+        email_bcc = []
 
-    if not ssl:
-        ssl = config.get('smtp_ssl', False)
+    if not attach:
+        msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
+    else:
+        msg = MIMEMultipart()
 
-    msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
+    msg['Subject'] = Header(ustr(subject), 'utf-8')
     msg['From'] = email_from
     del msg['Reply-To']
     if reply_to:
         msg['Reply-To'] = reply_to
+    else:
+        msg['Reply-To'] = msg['From']
     msg['To'] = COMMASPACE.join(email_to)
     if email_cc:
         msg['Cc'] = COMMASPACE.join(email_cc)
     if email_bcc:
         msg['Bcc'] = COMMASPACE.join(email_bcc)
-    if tinycrm:
-        msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(tinycrm)+'@'+socket.gethostname()+'>'
     msg['Date'] = formatdate(localtime=True)
-    msg.attach( MIMEText(body or '', _charset='utf-8', _subtype="html"))
-    for (fname,fcontent) in attach:
-        part = MIMEBase('application', "octet-stream")
-        part.set_payload( fcontent )
-        Encoders.encode_base64(part)
-        part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
-        msg.attach(part)
+
+    # Add OpenERP Server information
+    msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)'
+    msg['X-OpenERP-Server-Host'] = socket.gethostname()
+    msg['X-OpenERP-Server-Version'] = release.version
+
+    # Add dynamic X Header
+    for key, value in x_headers.items():
+        msg['X-OpenERP-%s' % key] = str(value)
+
+    if tinycrm:
+        msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname())
+
+    if attach:
+        msg.attach( MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
+
+        for (fname,fcontent) in attach:
+            part = MIMEBase('application', "octet-stream")
+            part.set_payload( fcontent )
+            Encoders.encode_base64(part)
+            part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
+            msg.attach(part)
     try:
         s = smtplib.SMTP()
 
@@ -427,13 +381,16 @@ def email_send_attach(email_from, email_to, subject, body, email_cc=None, email_
 
         if config['smtp_user'] or config['smtp_password']:
             s.login(config['smtp_user'], config['smtp_password'])
-        s.sendmail(email_from, flatten([email_to, email_cc, email_bcc]), msg.as_string())
+
+        s.sendmail(email_from, 
+                   flatten([email_to, email_cc, email_bcc]), 
+                   msg.as_string()
+                  )
         s.quit()
     except Exception, e:
         import logging
         logging.getLogger().error(str(e))
         return False
-
     return True
 
 #----------------------------------------------------------
@@ -442,10 +399,11 @@ def email_send_attach(email_from, email_to, subject, body, email_cc=None, email_
 # text must be latin-1 encoded
 def sms_send(user, password, api_id, text, to):
     import urllib
-    params = urllib.urlencode({'user': user, 'password': password, 'api_id': api_id, 'text': text, 'to':to})
-    #f = urllib.urlopen("http://api.clickatell.com/http/sendmsg", params)
-    f = urllib.urlopen("http://196.7.150.220/http/sendmsg", params)
-    print f.read()
+    url = "http://api.urlsms.com/SendSMS.aspx"
+    #url = "http://196.7.150.220/http/sendmsg"
+    params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
+    f = urllib.urlopen(url+"?"+params)
+    # FIXME: Use the logger if there is an error
     return True
 
 #---------------------------------------------------------
@@ -592,81 +550,236 @@ def is_hashable(h):
     except TypeError:
         return False
 
-#
-# Use it as a decorator of the function you plan to cache
-# Timeout: 0 = no timeout, otherwise in seconds
-#
 class cache(object):
-    def __init__(self, timeout=10000, skiparg=2):
-        self.timeout = timeout
+    """
+    Use it as a decorator of the function you plan to cache
+    Timeout: 0 = no timeout, otherwise in seconds
+    """
+    
+    __caches = []
+    
+    def __init__(self, timeout=None, skiparg=2, multi=None):
+        assert skiparg >= 2 # at least self and cr
+        if timeout is None:
+            self.timeout = config['cache_timeout']
+        else:
+            self.timeout = timeout
+        self.skiparg = skiparg
+        self.multi = multi
+        self.lasttime = time.time()
         self.cache = {}
+        self.fun = None 
+        cache.__caches.append(self)
+
+    
+    def _generate_keys(self, dbname, kwargs2):
+        """
+        Generate keys depending of the arguments and the self.mutli value
+        """
+        
+        def to_tuple(d):
+            i = d.items()
+            i.sort(key=lambda (x,y): x)
+            return tuple(i)
+
+        if not self.multi:
+            key = (('dbname', dbname),) + to_tuple(kwargs2)
+            yield key, None
+        else:
+            multis = kwargs2[self.multi][:]    
+            for id in multis:
+                kwargs2[self.multi] = (id,)
+                key = (('dbname', dbname),) + to_tuple(kwargs2)
+                yield key, id
+    
+    def _unify_args(self, *args, **kwargs):
+        # Update named arguments with positional argument values (without self and cr)
+        kwargs2 = self.fun_default_values.copy()
+        kwargs2.update(kwargs)
+        kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
+        for k in kwargs2:
+            if isinstance(kwargs2[k], (list, dict, set)):
+                kwargs2[k] = tuple(kwargs2[k])
+            elif not is_hashable(kwargs2[k]):
+                kwargs2[k] = repr(kwargs2[k])
+
+        return kwargs2
+    
+    def clear(self, dbname, *args, **kwargs):
+        """clear the cache for database dbname
+            if *args and **kwargs are both empty, clear all the keys related to this database
+        """
+        if not args and not kwargs:
+            keys_to_del = [key for key in self.cache if key[0][1] == dbname]
+        else:
+            kwargs2 = self._unify_args(*args, **kwargs)
+            keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
+        
+        for key in keys_to_del:
+            del self.cache[key]
+    
+    @classmethod
+    def clean_caches_for_db(cls, dbname):
+        for c in cls.__caches:
+            c.clear(dbname)
 
     def __call__(self, fn):
-        arg_names = inspect.getargspec(fn)[0][2:]
-        def cached_result(self2, cr=None, *args, **kwargs):
-            if cr is None:
-                self.cache = {}
-                return True
-
-            # Update named arguments with positional argument values
-            kwargs.update(dict(zip(arg_names, args)))
-            for k in kwargs:
-                if isinstance(kwargs[k], (list, dict, set)):
-                    kwargs[k] = tuple(kwargs[k])
-                elif not is_hashable(kwargs[k]):
-                    kwargs[k] = repr(kwargs[k])
-            kwargs = kwargs.items()
-            kwargs.sort()
-
-            # Work out key as a tuple of ('argname', value) pairs
-            key = (('dbname', cr.dbname),) + tuple(kwargs)
-
-            # Check cache and return cached value if possible
-            if key in self.cache:
-                (value, last_time) = self.cache[key]
-                mintime = time.time() - self.timeout
-                if self.timeout <= 0 or mintime <= last_time:
-                    return value
-
-            # Work out new value, cache it and return it
-            # FIXME Should copy() this value to avoid futur modifications of the cache ?
-            # FIXME What about exceptions ?
-            result = fn(self2,cr,**dict(kwargs))
-
-            self.cache[key] = (result, time.time())
+        if self.fun is not None:
+            raise Exception("Can not use a cache instance on more than one function")
+        self.fun = fn
+
+        argspec = inspect.getargspec(fn)
+        self.fun_arg_names = argspec[0][self.skiparg:]
+        self.fun_default_values = {}
+        if argspec[3]:
+            self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
+        
+        def cached_result(self2, cr, *args, **kwargs):
+            if time.time()-self.timeout > self.lasttime:
+                self.lasttime = time.time()
+                t = time.time()-self.timeout 
+                for key in self.cache.keys():
+                    if self.cache[key][1]<t:
+                        del self.cache[key]
+
+            kwargs2 = self._unify_args(*args, **kwargs)
+
+            result = {}
+            notincache = {}
+            for key, id in self._generate_keys(cr.dbname, kwargs2):
+                if key in self.cache:
+                    result[id] = self.cache[key][0]
+                else:
+                    notincache[id] = key
+            
+            if notincache:
+                if self.multi:
+                    kwargs2[self.multi] = notincache.keys()
+                
+                result2 = fn(self2, cr, *args[2:self.skiparg], **kwargs2)
+                if not self.multi:
+                    key = notincache[None]
+                    self.cache[key] = (result2, time.time())
+                    result[None] = result2
+                else:
+                    for id in result2:
+                        key = notincache[id]
+                        self.cache[key] = (result2[id], time.time())
+                    result.update(result2)
+                        
+            if not self.multi:
+                return result[None]
             return result
+
+        cached_result.clear_cache = self.clear
         return cached_result
 
 def to_xml(s):
     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
 
+def ustr(value):
+    """This method is similar to the builtin `str` method, except
+    it will return Unicode string.
+
+    @param value: the value to convert
+
+    @rtype: unicode
+    @return: unicode string
+    """
+
+    if isinstance(value, unicode):
+        return value
+
+    if hasattr(value, '__unicode__'):
+        return unicode(value)
+
+    if not isinstance(value, str):
+        value = str(value)
+
+    try: # first try utf-8
+        return unicode(value, 'utf-8')
+    except:
+        pass
+
+    try: # then extened iso-8858
+        return unicode(value, 'iso-8859-15')
+    except:
+        pass
+
+    # else use default system locale
+    from locale import getlocale
+    return unicode(value, getlocale()[1])
+
+def exception_to_unicode(e):
+    if hasattr(e, 'message'):
+        return ustr(e.message)
+    if hasattr(e, 'args'):
+        return "\n".join((ustr(a) for a in e.args))
+    try:
+        return ustr(e)
+    except:
+        return u"Unknow message"
+
+
+# to be compatible with python 2.4
+import __builtin__
+if not hasattr(__builtin__, 'all'):
+    def all(iterable):
+        for element in iterable:
+            if not element:
+               return False
+        return True
+        
+    __builtin__.all = all
+    del all
+    
+if not hasattr(__builtin__, 'any'):
+    def any(iterable):
+        for element in iterable:
+            if element:
+               return True
+        return False
+        
+    __builtin__.any = any
+    del any
+
+
+
 def get_languages():
     languages={
+        'ar_AR': u'Arabic / الْعَرَبيّة',
         'bg_BG': u'Bulgarian / български',
+        'bs_BS': u'Bosnian / bosanski jezik',
         'ca_ES': u'Catalan / Català',
         'cs_CZ': u'Czech / Čeština',
+        'da_DK': u'Danish / Dansk',
         'de_DE': u'German / Deutsch',
+        'el_EL': u'Greek / Ελληνικά',
         'en_CA': u'English (CA)',
         'en_EN': u'English (default)',
         'en_GB': u'English (UK)',
         'en_US': u'English (US)',
         'es_AR': u'Spanish (AR) / Español (AR)',
         'es_ES': u'Spanish / Español',
-        'et_ET': u'Estonian / Eesti keel',
+        'et_EE': u'Estonian / Eesti keel',
         'fr_BE': u'French (BE) / Français (BE)',
         'fr_CH': u'French (CH) / Français (CH)',
         'fr_FR': u'French / Français',
         'hr_HR': u'Croatian / hrvatski jezik',
         'hu_HU': u'Hungarian / Magyar',
+        'id_ID': u'Indonesian / Bahasa Indonesia',
         'it_IT': u'Italian / Italiano',
         'lt_LT': u'Lithuanian / Lietuvių kalba',
         'nl_NL': u'Dutch / Nederlands',
+        'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
+        'pl_PL': u'Polish / Język polski',
         'pt_BR': u'Portugese (BR) / português (BR)',
         'pt_PT': u'Portugese / português',
         'ro_RO': u'Romanian / limba română',
         'ru_RU': u'Russian / русский язык',
         'sl_SL': u'Slovenian / slovenščina',
         'sv_SE': u'Swedish / svenska',
+        'tr_TR': u'Turkish / Türkçe',
         'uk_UK': u'Ukrainian / украї́нська мо́ва',
         'zh_CN': u'Chinese (CN) / 简体中文' ,
         'zh_TW': u'Chinese (TW) / 正體字',
@@ -690,7 +803,7 @@ def get_user_companies(cr, user):
         res=[x[0] for x in cr.fetchall()]
         res.extend(_get_company_children(cr, res))
         return res
-    cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %d AND comp.id = u.company_id' % (user,))
+    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,))
     compids=[cr.fetchone()[0]]
     compids.extend(_get_company_children(cr, compids))
     return compids
@@ -726,39 +839,85 @@ def human_size(sz):
         i = i + 1
     return "%0.2f %s" % (s, units[i])
 
-def logged(when):
-    def log(f, res, *args, **kwargs):
-        vector = ['Call -> function: %s' % f]
+def logged(f):
+    from tools.func import wraps
+    
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        import netsvc
+        from pprint import pformat
+
+        vector = ['Call -> function: %r' % f]
         for i, arg in enumerate(args):
-            vector.append( '  arg %02d: %r' % ( i, arg ) )
+            vector.append('  arg %02d: %s' % (i, pformat(arg)))
         for key, value in kwargs.items():
-            vector.append( '  kwarg %10s: %r' % ( key, value ) )
-        vector.append( '  result: %r' % res )
-        print "\n".join(vector)
+            vector.append('  kwarg %10s: %s' % (key, pformat(value)))
+
+        timeb4 = time.time()
+        res = f(*args, **kwargs)
+        
+        vector.append('  result: %s' % pformat(res))
+        vector.append('  time delta: %s' % (time.time() - timeb4))
+        netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
+        return res
 
-    def pre_logged(f):
-        def wrapper(*args, **kwargs):
-            res = f(*args, **kwargs)
-            log(f, res, *args, **kwargs)
-            return res
-        return wrapper
+    return wrapper
+
+class profile(object):
+    def __init__(self, fname=None):
+        self.fname = fname
 
-    def post_logged(f):
+    def __call__(self, f):
+        from tools.func import wraps
+
+        @wraps(f)
         def wrapper(*args, **kwargs):
-            now = time.time()
-            res = None
-            try:
-                res = f(*args, **kwargs)
-                return res
-            finally:
-                log(f, res, *args, **kwargs)
-                print "  time delta: %s" % (time.time() - now)
+            class profile_wrapper(object):
+                def __init__(self):
+                    self.result = None
+                def __call__(self):
+                    self.result = f(*args, **kwargs)
+            pw = profile_wrapper()
+            import cProfile
+            fname = self.fname or ("%s.cprof" % (f.func_name,))
+            cProfile.runctx('pw()', globals(), locals(), filename=fname)
+            return pw.result
+
         return wrapper
 
-    try:
-        return { "pre" : pre_logged, "post" : post_logged}[when]
-    except KeyError, e:
-        raise ValueError(e), "must to be 'pre' or 'post'"
+def debug(what):
+    """
+        This method allow you to debug your code without print
+        Example:
+        >>> def func_foo(bar)
+        ...     baz = bar
+        ...     debug(baz)
+        ...     qnx = (baz, bar)
+        ...     debug(qnx)
+        ...
+        >>> func_foo(42)
+
+        This will output on the logger:
+        
+            [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
+            [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
+
+        To view the DEBUG lines in the logger you must start the server with the option
+            --log-level=debug
+
+    """
+    import netsvc
+    from inspect import stack
+    import re
+    from pprint import pformat
+    st = stack()[1]
+    param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
+    while param.count(')') > param.count('('): param = param[:param.rfind(')')]
+    what = pformat(what)
+    if param != what:
+        what = "%s = %s" % (param, what)
+    netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
+
 
 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
@@ -788,6 +947,26 @@ icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_
 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
 ])
 
+def extract_zip_file(zip_file, outdirectory):
+    import zipfile
+    import os
+
+    zf = zipfile.ZipFile(zip_file, 'r')
+    out = outdirectory
+    for path in zf.namelist():
+        tgt = os.path.join(out, path)
+        tgtdir = os.path.dirname(tgt)
+        if not os.path.exists(tgtdir):
+            os.makedirs(tgtdir)
+
+        if not tgt.endswith(os.sep):
+            fp = open(tgt, 'wb')
+            fp.write(zf.read(path))
+            fp.close()
+    zf.close()
+
+
+
 
 
 if __name__ == '__main__':