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