util.py 9.4 KB
Newer Older
Zach's avatar
Zach committed
1 2
###############################################################################
##
3
##  Copyright (C) 2011-2014 Tavendo GmbH
Zach's avatar
Zach committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
##
##  Licensed under the Apache License, Version 2.0 (the "License");
##  you may not use this file except in compliance with the License.
##  You may obtain a copy of the License at
##
##      http://www.apache.org/licenses/LICENSE-2.0
##
##  Unless required by applicable law or agreed to in writing, software
##  distributed under the License is distributed on an "AS IS" BASIS,
##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
##  See the License for the specific language governing permissions and
##  limitations under the License.
##
###############################################################################

19 20
from __future__ import absolute_import

Zach's avatar
Zach committed
21 22 23
__all__ = ("utcnow",
           "parseutc",
           "utcstr",
24
           "id",
Zach's avatar
Zach committed
25 26
           "newid",
           "rtime",
27
           "Stopwatch",
28 29
           "Tracker",
           "EqualityMixin")
30

Zach's avatar
Zach committed
31 32 33 34

import time
import random
import sys
35
from datetime import datetime, timedelta
36
from pprint import pformat
Zach's avatar
Zach committed
37 38 39 40 41 42



def utcnow():
   """
   Get current time in UTC as ISO 8601 string.
43

44 45
   :returns: Current time as string in ISO 8601 format.
   :rtype: unicode
46 47 48 49 50 51 52 53 54 55
   """
   now = datetime.utcnow()
   return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"



def utcstr(ts):
   """
   Format UTC timestamp in ISO 8601 format.

56 57 58 59 60
   :param ts: The timestamp to format.
   :type ts: instance of :py:class:`datetime.datetime`

   :returns: Timestamp formatted in ISO 8601 format.
   :rtype: unicode
Zach's avatar
Zach committed
61
   """
62 63 64 65 66
   if ts:
      return ts.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
   else:
      return ts

Zach's avatar
Zach committed
67 68


69
def parseutc(datestr):
Zach's avatar
Zach committed
70
   """
71
   Parse an ISO 8601 combined date and time string, like i.e. ``"2011-11-23T12:23:00Z"``
Zach's avatar
Zach committed
72
   into a UTC datetime instance.
73

74 75 76 77 78 79 80 81
   .. deprecated:: 0.8.12
      Use the **iso8601** module instead (e.g. ``iso8601.parse_date("2014-05-23T13:03:44.123Z")``)

   :param datestr: The datetime string to parse.
   :type datestr: unicode

   :returns: The converted datetime object.
   :rtype: instance of :py:class:`datetime.datetime`
Zach's avatar
Zach committed
82 83
   """
   try:
84
      return datetime.strptime(datestr, "%Y-%m-%dT%H:%M:%SZ")
Zach's avatar
Zach committed
85 86 87 88
   except:
      return None


89 90

def id():
Zach's avatar
Zach committed
91
   """
92 93 94 95 96
   Generate a new random object ID from range **[0, 2**53]**.

   The upper bound **2**53** is chosen since it is the maximum integer that can be
   represented as a IEEE double such that all smaller integers are representable as well.

97
   Hence, IDs can be safely used with languages that use IEEE double as their
98 99 100 101
   main (or only) number type (JavaScript, Lua, etc).

   :returns: A random object ID.
   :rtype: int
Zach's avatar
Zach committed
102
   """
103
   return random.randint(0, 9007199254740992)
Zach's avatar
Zach committed
104 105


106 107

def newid(len = 16):
Zach's avatar
Zach committed
108 109
   """
   Generate a new random object ID.
110 111 112 113 114 115

   :param len: The length (in chars) of the ID to generate.
   :type len: int

   :returns: A random object ID.
   :rtype: str
Zach's avatar
Zach committed
116
   """
117
   return ''.join([random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") for _ in xrange(len)])
Zach's avatar
Zach committed
118 119 120 121 122 123 124 125 126 127 128



## Select the most precise walltime measurement function available
## on the platform
##
if sys.platform.startswith('win'):
   ## On Windows, this function returns wall-clock seconds elapsed since the
   ## first call to this function, as a floating point number, based on the
   ## Win32 function QueryPerformanceCounter(). The resolution is typically
   ## better than one microsecond
129 130
   _rtime = time.clock
   _ = _rtime() # this starts wallclock
Zach's avatar
Zach committed
131 132 133 134 135
else:
   ## On Unix-like platforms, this used the first available from this list:
   ## (1) gettimeofday() -- resolution in microseconds
   ## (2) ftime() -- resolution in milliseconds
   ## (3) time() -- resolution in seconds
136 137
   _rtime = time.time

Zach's avatar
Zach committed
138

139 140 141 142 143 144 145 146
rtime = _rtime
"""
Precise wallclock time.

:returns: The current wallclock in seconds. Returned values are only guaranteed
   to be meaningful relative to each other.
:rtype: float
"""
Zach's avatar
Zach committed
147

148

Zach's avatar
Zach committed
149 150
class Stopwatch:
   """
151 152 153 154 155
   Stopwatch based on walltime.

   This can be used to do code timing and uses the most precise walltime measurement
   available on the platform. This is a very light-weight object,
   so create/dispose is very cheap.
Zach's avatar
Zach committed
156 157 158 159
   """

   def __init__(self, start = True):
      """
160 161
      :param start: If ``True``, immediately start the stopwatch.
      :type start: bool
Zach's avatar
Zach committed
162 163 164 165 166 167 168 169 170 171 172 173
      """
      self._elapsed = 0
      if start:
         self._started = rtime()
         self._running = True
      else:
         self._started = None
         self._running = False

   def elapsed(self):
      """
      Return total time elapsed in seconds during which the stopwatch was running.
174 175 176

      :returns: The elapsed time in seconds.
      :rtype: float
Zach's avatar
Zach committed
177 178 179 180 181 182 183 184 185 186 187
      """
      if self._running:
         now = rtime()
         return self._elapsed + (now - self._started)
      else:
         return self._elapsed

   def pause(self):
      """
      Pauses the stopwatch and returns total time elapsed in seconds during which
      the stopwatch was running.
188 189 190

      :returns: The elapsed time in seconds.
      :rtype: float
Zach's avatar
Zach committed
191 192 193 194 195 196 197 198 199 200 201 202 203
      """
      if self._running:
         now = rtime()
         self._elapsed += now - self._started
         self._running = False
         return self._elapsed
      else:
         return self._elapsed

   def resume(self):
      """
      Resumes a paused stopwatch and returns total elapsed time in seconds
      during which the stopwatch was running.
204 205 206

      :returns: The elapsed time in seconds.
      :rtype: float
Zach's avatar
Zach committed
207 208 209 210 211 212 213 214 215 216 217 218 219
      """
      if not self._running:
         self._started = rtime()
         self._running = True
         return self._elapsed
      else:
         now = rtime()
         return self._elapsed + (now - self._started)

   def stop(self):
      """
      Stops the stopwatch and returns total time elapsed in seconds during which
      the stopwatch was (previously) running.
220 221 222

      :returns: The elapsed time in seconds.
      :rtype: float
Zach's avatar
Zach committed
223 224 225 226 227 228
      """
      elapsed = self.pause()
      self._elapsed = 0
      self._started = None
      self._running = False
      return elapsed
229 230 231 232



class Tracker:
233 234 235
   """
   A key-based statistics tracker.
   """
236 237 238 239 240 241 242

   def __init__(self, tracker, tracked):
      """
      """
      self.tracker = tracker
      self.tracked = tracked
      self._timings = {}
243 244
      self._offset = rtime()
      self._dt_offset = datetime.utcnow()
245 246 247 248 249 250 251 252 253


   def track(self, key):
      """
      Track elapsed for key.

      :param key: Key under which to track the timing.
      :type key: str
      """
254
      self._timings[key] = rtime()
255 256 257 258 259 260 261 262 263 264


   def diff(self, startKey, endKey, format = True):
      """
      Get elapsed difference between two previously tracked keys.

      :param startKey: First key for interval (older timestamp).
      :type startKey: str
      :param endKey: Second key for interval (younger timestamp).
      :type endKey: str
265
      :param format: If ``True``, format computed time period and return string.
266 267
      :type format: bool

268 269
      :returns: Computed time period in seconds (or formatted string).
      :rtype: float or str
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
      """
      if endKey in self._timings and startKey in self._timings:
         d = self._timings[endKey] - self._timings[startKey]
         if format:
            if d < 0.00001: # 10us
               s = "%d ns" % round(d * 1000000000.)
            elif d < 0.01: # 10ms
               s = "%d us" % round(d * 1000000.)
            elif d < 10: # 10s
               s = "%d ms" % round(d * 1000.)
            else:
               s = "%d s" % round(d)
            return s.rjust(8)
         else:
            return d
      else:
         if format:
            return "n.a.".rjust(8)
         else:
            return None

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
   def absolute(self, key):
      """
      Return the UTC wall-clock time at which a tracked event occurred.

      :param key: The key
      :type key: str

      :returns: Timezone-naive datetime.
      :rtype: instance of :py:class:`datetime.datetime`
      """
      elapsed = self[key]
      if elapsed is None:
         raise KeyError("No such key \"%s\"." % elapsed)
      return self._dt_offset + timedelta(seconds=elapsed)

306 307

   def __getitem__(self, key):
308 309 310 311
      if key in self._timings:
         return self._timings[key] - self._offset
      else:
         return None
312 313 314


   def __iter__(self):
315
      return self._timings.__iter__()
316 317 318 319 320 321 322 323


   def __str__(self):
      return pformat(self._timings)



class EqualityMixin:
324 325 326 327 328 329 330 331
   """
   Mixing to add equality comparison operators to a class.

   Two objects are identical under this mixin, if and only if:

   1. both object have the same class
   2. all non-private object attributes are equal
   """
332 333

   def __eq__(self, other):
334 335 336 337 338 339 340 341 342
      """
      Compare this object to another object for equality.

      :param other: The other object to compare with.
      :type other: obj

      :returns: ``True`` iff the objects are equal.
      :rtype: bool
      """
343 344 345 346 347 348 349 350 351 352 353 354
      if not isinstance(other, self.__class__):
         return False
      # we only want the actual message data attributes (not eg _serialize)
      for k in self.__dict__:
         if not k.startswith('_'):
            if not self.__dict__[k] == other.__dict__[k]:
               return False
      return True
      #return (isinstance(other, self.__class__) and self.__dict__ == other.__dict__)


   def __ne__(self, other):
355 356 357 358 359 360 361 362 363
      """
      Compare this object to another object for inequality.

      :param other: The other object to compare with.
      :type other: obj

      :returns: ``True`` iff the objects are not equal.
      :rtype: bool
      """
364
      return not self.__eq__(other)