utils.py 8.82 KB
Newer Older
1
## 
2
## Created : Tue Jul 26 06:54:41 IST 2011
3
## 
4
## Copyright (C) 2011, 2012, 2013 by Sriram Karra <karra.etc@gmail.com>
5
## 
6 7 8
## This file is part of ASynK
##
## ASynK is free software: you can redistribute it and/or modify it under
9
## the terms of the GNU Affero GPL (GNU AGPL) as published by the
10 11 12 13 14 15 16 17 18 19
## Free Software Foundation, version 3 of the License
##
## ASynK is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
## License for more details.
##
## You should have a copy of the license in the doc/ directory of ASynK.  If
## not, see <http://www.gnu.org/licenses/>.
##
20

21
import iso8601, logging, os, re, xml.dom.minidom
22

23 24
time_start = "1980-01-01T00:00:00.00+00:00"

25 26
def yyyy_mm_dd_to_pytime (date_str):
    ## FIXME: Temporary hack to ensure we have a yyyy-mm-dd format. Google
27
    ## allows the year to be skipped. Outlook creates a problem. We bridge the
28
    ## gap by inserting '1887' (birth year of Srinivasa Ramanujan)
29 30 31

    import pywintypes

32 33 34 35 36 37 38 39 40 41 42 43 44 45
    res = re.search('--(\d\d)-(\d\d)', date_str)
    if res:
        date_str = '1887-%s-%s' % (res.group(1), res.group(2))

    dt = datetime.strptime(date_str, '%Y-%m-%d')
    return pywintypes.Time(dt.timetuple())

def pytime_to_yyyy_mm_dd (pyt):
    if pyt.year == 1887:
        ## Undo the hack noted above.
        return ('--%02d-%02d' % (pyt.month, pyt.day))
    else:
        return ('%04d-%02d-%02d' % (pyt.year, pyt.month, pyt.day))

46
## Some Global Variables to get started
47
asynk_ver = 'v2.2.0-rc1'
48 49 50

def asynk_ver_str ():
    return 'ASynK %s' % asynk_ver
51

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
def asynk_ts_to_iso8601 (ts):
    """The text timestamps in ASynK will be stored in a format that is readily
    usable by BBDB. Frequently there is a need to parse it into other formats,
    and as an intermediate step we would like to convert it into iso8601
    format to leverage the libraries available for handling iso8601. This
    routine converts a text string in the internal ASynK (BBDB) text format,
    into iso8601 format with Zone Specifier."""

    ## FIXME: All of these assume the timestamps are in UTC. Bad things can
    ## happen if some other timezone is provided.
    try:
        ## Eliminate the case where the input string is already in iso8601
        ## format... 
        iso8601.parse(ts)
        return ts
    except ValueError, e:
        return re.sub(r'(\d\d\d\d-\d\d-\d\d) (\d\d:\d\d:\d\d).*$',
                      r'\1T\2Z', ts)

def asynk_ts_parse (ts):
    """For historical reasons (IOW Bugs), ASynK versions have stored created
    and modified timestamps in two distinct text representations. This routine
    is a wrapper to gracefully handle both cases, convert it into a iso
    string, and then parse it into a python datetime object, which is returned
    """

    return iso8601.parse(asynk_ts_to_iso8601(ts))
79

80 81 82 83 84 85
def touch (fn):
    """Equivalent of the Unix 'touch' command."""

    with open(fn, 'a'):
        os.utime(fn, None)

86
def abs_pathname (config, fname):
87 88 89 90
    """If fname is an absolute path then it is returned as is. If it starts
    with a ~ then expand the path as per Unix conventions and finally if it
    appears to be a relative path, the application root is prepended to the
    name and an absolute OS-specific path string is returned."""
91 92

    app_root = config.get_app_root()
93 94 95
    if fname[0] == '~':
        return os.path.expanduser(fname)

96 97 98 99
    if fname[0] != '/' and fname[1] != ':' and fname[2] != '\\':
        return os.path.join(app_root, fname)

    return fname
100

101 102
def chompq (s):
    """Remove any leading and trailing quotes from the passed string."""
103
    if len(s) < 2:
104 105 106 107 108 109 110
        return s

    if s[0] == '"' and s[len(s)-1] == '"':
        return s[1:len(s)-1]
    else:
        return s

111
def unchompq (s):
112 113 114 115
    if s:
        return '"' + unicode(s) + '"'
    else:
        return '""'
116

117 118 119 120 121 122 123 124 125
## The follow is a super cool implementation of enum equivalent in
## Python. Taken with a lot of gratitude from this post on Stackoverflow:
## http://stackoverflow.com/a/1695250/987738
##
## It is used like so: Numbers = enum(ONE=1, TWO=2, THREE='three')
## and, Numbers = enum('ZERO', 'ONE', 'TWO') # for auto initialization
def enum(*sequential, **named):
	enums = dict(zip(sequential, range(len(sequential))), **named)
	return type('Enum', (), enums)
126

127
def get_link_rel (links, rel):
128 129 130 131
    """From a Google data entry links array, fetch the link with the
    specifirf 'rel' attribute. examples of values for 'rel' could be:
    self, edit, etc."""

132 133 134 135 136
    for link in links:
        if link.rel == rel:
            return link.href

    return None
137

138 139 140 141 142 143 144 145 146 147 148
def get_event_rel (events, rel):
    """From a Google data entry events array, fetch and return the first event
    with the specified 'rel' attribute. examples of values for 'rel' could be:
    anniversary, etc."""

    for event in events:
        if event.rel == rel:
            return event.when

    return None

149 150 151
from   datetime import tzinfo, timedelta, datetime
import time as _time

152 153 154 155 156 157 158 159 160 161 162 163 164
def del_files_older_than (abs_dir, days):
    """Delete all files in abs_dir/ that were modified more than 'days' days
    or earlier."""

    now = _time.time()

    for f in os.listdir(abs_dir):
        fi = os.path.join(abs_dir, f)
        if os.stat(fi).st_mtime < now - days * 86400:
            if os.path.isfile(fi):
                logging.debug('Deleting File: %s...', f)
                os.remove(fi)

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
# A class capturing the platform's idea of local time.

ZERO = timedelta(0)
STDOFFSET = timedelta(seconds = -_time.timezone)
if _time.daylight:
    DSTOFFSET = timedelta(seconds = -_time.altzone)
else:
    DSTOFFSET = STDOFFSET

DSTDIFF = DSTOFFSET - STDOFFSET

class LocalTimezone(tzinfo):

    def utcoffset(self, dt):
        if self._isdst(dt):
            return DSTOFFSET
        else:
            return STDOFFSET

    def dst(self, dt):
        if self._isdst(dt):
            return DSTDIFF
        else:
            return ZERO

    def tzname(self, dt):
        return _time.tzname[self._isdst(dt)]

    def _isdst(self, dt):
        tt = (dt.year, dt.month, dt.day,
              dt.hour, dt.minute, dt.second,
              dt.weekday(), 0, -1)
        stamp = _time.mktime(tt)
        tt = _time.localtime(stamp)
        return tt.tm_isdst > 0

localtz = LocalTimezone()

203 204 205
def utc_time_to_local_ts (t, ret_dt=False):
    """Convert a Pytime object which is in UTC into a timestamp in local
    timezone.
206

207 208
    If dt is True, then a datetime object is returned, else (this is the
    default), an integer representing time since the epoch is returned."""
209

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
    utc_off = localtz.utcoffset(datetime.now())
    try:
        utc_ts  = int(t)
        dt = datetime.fromtimestamp(utc_ts)
    except ValueError, e:
        ## Pytimes earlier than the epoch are a pain in the rear end. 
        dt = datetime(year=t.year,
                      month=t.month,
                      day=t.day,
                      hour=t.hour,
                      minute=t.minute,
                      second=t.second)

    d = dt + utc_off
    if ret_dt:
        return d
    else:
        return _time.mktime(d.timetuple())
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243

def classify_email_addr (addr, domains):
    """Return a tuple of (home, work, other) booleans classifying if the
    specified address falls within one of the domains."""

    res = {'home' : False, 'work' : False, 'other' : False}

    for cat in res.keys():
        try:
            for domain in domains[cat]:
                if re.search((domain + '$'), addr):
                    res[cat] = True
        except KeyError, e:
            logging.warning('Invalid email_domains specification.')

    return (res['home'], res['work'], res['other'])
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292

##
## Some XML parsing and manipulation routines
##

def pretty_xml (x):
    x = xml.dom.minidom.parseString(x).toprettyxml()
    lines = x.splitlines()
    lines = [s for s in lines if not re.match(s.strip(), '^\s*$')]
    return os.linesep.join(lines)

def find_first_child (root, tag, ret='text'):
    """
    Look for the first child of root with specified tag and return it. If
    ret is 'text' then the value of the node is returned, else, the node
    is returned as an element.
    """

    for child in root.iter(tag):
        if ret == 'text':
            return child.text
        else:
            return child

    return None

GNS0_NAMESPACE="http://www.w3.org/2005/Atom"
GNS1_NAMESPACE="http://schemas.google.com/g/2005"
GNS2_NAMESPACE="http://schemas.google.com/contact/2008"
GNS3_NAMESPACE="http://schemas.google.com/gdata/batch"

def unQName (name):
    res = re.match('{.*}(.*)', name)
    return name if res is None else res.group(1)

def QName (namespace, name):
    return '{%s}%s' % (namespace, name)

def QName_GNS0 (name):
    return QName(GNS0_NAMESPACE, name)

def QName_GNS1 (name):
    return QName(GNS1_NAMESPACE, name)

def QName_GNS2 (name):
    return QName(GNS2_NAMESPACE, name)

def QName_GNS3 (name):
    return QName(GNS3_NAMESPACE, name)