1# -*- coding: utf-8 -*-
2"""
3    logbook.helpers
4    ~~~~~~~~~~~~~~~
5
6    Various helper functions
7
8    :copyright: (c) 2010 by Armin Ronacher, Georg Brandl.
9    :license: BSD, see LICENSE for more details.
10"""
11import os
12import re
13import sys
14import errno
15import time
16import random
17from datetime import datetime, timedelta
18
19PY2 = sys.version_info[0] == 2
20
21if PY2:
22    import __builtin__ as _builtins
23else:
24    import builtins as _builtins
25
26try:
27    import json
28except ImportError:
29    import simplejson as json
30
31if PY2:
32    from cStringIO import StringIO
33    iteritems = dict.iteritems
34    from itertools import izip as zip
35    xrange = _builtins.xrange
36else:
37    from io import StringIO
38    zip = _builtins.zip
39    xrange = range
40    iteritems = dict.items
41
42_IDENTITY = lambda obj: obj
43
44if PY2:
45    def u(s):
46        return unicode(s, "unicode_escape")
47else:
48    u = _IDENTITY
49
50if PY2:
51    integer_types = (int, long)
52    string_types = (basestring,)
53else:
54    integer_types = (int,)
55    string_types = (str,)
56
57if PY2:
58    import httplib as http_client
59else:
60    from http import client as http_client
61
62if PY2:
63    # Yucky, but apparently that's the only way to do this
64    exec("""
65def reraise(tp, value, tb=None):
66    raise tp, value, tb
67""", locals(), globals())
68else:
69    def reraise(tp, value, tb=None):
70        if value.__traceback__ is not tb:
71            raise value.with_traceback(tb)
72        raise value
73
74
75# this regexp also matches incompatible dates like 20070101 because
76# some libraries (like the python xmlrpclib modules) use this
77_iso8601_re = re.compile(
78    # date
79    r'(\d{4})(?:-?(\d{2})(?:-?(\d{2}))?)?'
80    # time
81    r'(?:T(\d{2}):(\d{2})(?::(\d{2}(?:\.\d+)?))?(Z|[+-]\d{2}:\d{2})?)?$'
82)
83_missing = object()
84if PY2:
85    def b(x):
86        return x
87
88    def _is_text_stream(x):
89        return True
90else:
91    import io
92
93    def b(x):
94        return x.encode('ascii')
95
96    def _is_text_stream(stream):
97        return isinstance(stream, io.TextIOBase)
98
99
100can_rename_open_file = False
101if os.name == 'nt':
102    try:
103        import ctypes
104
105        _MOVEFILE_REPLACE_EXISTING = 0x1
106        _MOVEFILE_WRITE_THROUGH = 0x8
107        _MoveFileEx = ctypes.windll.kernel32.MoveFileExW
108
109        def _rename(src, dst):
110            if PY2:
111                if not isinstance(src, unicode):
112                    src = unicode(src, sys.getfilesystemencoding())
113                if not isinstance(dst, unicode):
114                    dst = unicode(dst, sys.getfilesystemencoding())
115            if _rename_atomic(src, dst):
116                return True
117            retry = 0
118            rv = False
119            while not rv and retry < 100:
120                rv = _MoveFileEx(src, dst, _MOVEFILE_REPLACE_EXISTING |
121                                 _MOVEFILE_WRITE_THROUGH)
122                if not rv:
123                    time.sleep(0.001)
124                    retry += 1
125            return rv
126
127        # new in Vista and Windows Server 2008
128        _CreateTransaction = ctypes.windll.ktmw32.CreateTransaction
129        _CommitTransaction = ctypes.windll.ktmw32.CommitTransaction
130        _MoveFileTransacted = ctypes.windll.kernel32.MoveFileTransactedW
131        _CloseHandle = ctypes.windll.kernel32.CloseHandle
132        can_rename_open_file = True
133
134        def _rename_atomic(src, dst):
135            ta = _CreateTransaction(None, 0, 0, 0, 0, 1000, 'Logbook rename')
136            if ta == -1:
137                return False
138            try:
139                retry = 0
140                rv = False
141                while not rv and retry < 100:
142                    rv = _MoveFileTransacted(src, dst, None, None,
143                                             _MOVEFILE_REPLACE_EXISTING |
144                                             _MOVEFILE_WRITE_THROUGH, ta)
145                    if rv:
146                        rv = _CommitTransaction(ta)
147                        break
148                    else:
149                        time.sleep(0.001)
150                        retry += 1
151                return rv
152            finally:
153                _CloseHandle(ta)
154    except Exception:
155        def _rename(src, dst):
156            return False
157
158        def _rename_atomic(src, dst):
159            return False
160
161    def rename(src, dst):
162        # Try atomic or pseudo-atomic rename
163        if _rename(src, dst):
164            return
165        # Fall back to "move away and replace"
166        try:
167            os.rename(src, dst)
168        except OSError:
169            e = sys.exc_info()[1]
170            if e.errno not in (errno.EEXIST, errno.EACCES):
171                raise
172            old = "%s-%08x" % (dst, random.randint(0, 2 ** 31 - 1))
173            os.rename(dst, old)
174            os.rename(src, dst)
175            try:
176                os.unlink(old)
177            except Exception:
178                pass
179else:
180    rename = os.rename
181    can_rename_open_file = True
182
183_JSON_SIMPLE_TYPES = (bool, float) + integer_types + string_types
184
185
186def to_safe_json(data):
187    """Makes a data structure safe for JSON silently discarding invalid
188    objects from nested structures.  This also converts dates.
189    """
190    def _convert(obj):
191        if obj is None:
192            return None
193        elif PY2 and isinstance(obj, str):
194            return obj.decode('utf-8', 'replace')
195        elif isinstance(obj, _JSON_SIMPLE_TYPES):
196            return obj
197        elif isinstance(obj, datetime):
198            return format_iso8601(obj)
199        elif isinstance(obj, list):
200            return [_convert(x) for x in obj]
201        elif isinstance(obj, tuple):
202            return tuple(_convert(x) for x in obj)
203        elif isinstance(obj, dict):
204            rv = {}
205            for key, value in iteritems(obj):
206                if not isinstance(key, string_types):
207                    key = str(key)
208                if not is_unicode(key):
209                    key = u(key)
210                rv[key] = _convert(value)
211            return rv
212    return _convert(data)
213
214
215def format_iso8601(d=None):
216    """Returns a date in iso8601 format."""
217    if d is None:
218        d = datetime.utcnow()
219    rv = d.strftime('%Y-%m-%dT%H:%M:%S')
220    if d.microsecond:
221        rv += '.' + str(d.microsecond)
222    return rv + 'Z'
223
224
225def parse_iso8601(value):
226    """Parse an iso8601 date into a datetime object.  The timezone is
227    normalized to UTC.
228    """
229    m = _iso8601_re.match(value)
230    if m is None:
231        raise ValueError('not a valid iso8601 date value')
232
233    groups = m.groups()
234    args = []
235    for group in groups[:-2]:
236        if group is not None:
237            group = int(group)
238        args.append(group)
239    seconds = groups[-2]
240    if seconds is not None:
241        if '.' in seconds:
242            sec, usec = seconds.split('.')
243            args.append(int(sec))
244            args.append(int(usec.ljust(6, '0')))
245        else:
246            args.append(int(seconds))
247
248    rv = datetime(*args)
249    tz = groups[-1]
250    if tz and tz != 'Z':
251        args = [int(x) for x in tz[1:].split(':')]
252        delta = timedelta(hours=args[0], minutes=args[1])
253        if tz[0] == '+':
254            rv -= delta
255        else:
256            rv += delta
257
258    return rv
259
260
261def get_application_name():
262    if not sys.argv or not sys.argv[0]:
263        return 'Python'
264    return os.path.basename(sys.argv[0]).title()
265
266
267class cached_property(object):
268    """A property that is lazily calculated and then cached."""
269
270    def __init__(self, func, name=None, doc=None):
271        self.__name__ = name or func.__name__
272        self.__module__ = func.__module__
273        self.__doc__ = doc or func.__doc__
274        self.func = func
275
276    def __get__(self, obj, type=None):
277        if obj is None:
278            return self
279        value = obj.__dict__.get(self.__name__, _missing)
280        if value is _missing:
281            value = self.func(obj)
282            obj.__dict__[self.__name__] = value
283        return value
284
285
286def get_iterator_next_method(it):
287    return lambda: next(it)
288
289
290# python 2 support functions and aliases
291def is_unicode(x):
292    if PY2:
293        return isinstance(x, unicode)
294    return isinstance(x, str)
295
296if PY2:
297    exec("""def with_metaclass(meta):
298    class _WithMetaclassBase(object):
299        __metaclass__ = meta
300    return _WithMetaclassBase
301""")
302else:
303    exec("""def with_metaclass(meta):
304    class _WithMetaclassBase(object, metaclass=meta):
305        pass
306    return _WithMetaclassBase
307""")
308