~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

fixup

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
18
#
19
19
 
 
20
from elementtree import ElementTree as ET
 
21
 
 
22
import base64
20
23
import cgi
21
24
import datetime
22
25
import logging
23
26
import re
24
27
import sha
 
28
import struct
 
29
import sys
25
30
import threading
26
 
 
27
 
import turbogears
 
31
import time
 
32
import traceback
28
33
 
29
34
 
30
35
log = logging.getLogger("loggerhead.controllers")
31
36
 
32
 
 
33
 
def timespan(delta):
34
 
    if delta.days > 730:
35
 
        # good grief!
36
 
        return '%d years' % (int(delta.days // 365.25),)
37
 
    if delta.days >= 3:
38
 
        return '%d days' % delta.days
39
 
    seg = []
40
 
    if delta.days > 0:
41
 
        if delta.days == 1:
42
 
            seg.append('1 day')
43
 
        else:
44
 
            seg.append('%d days' % delta.days)
45
 
    hrs = delta.seconds // 3600
46
 
    mins = (delta.seconds % 3600) // 60
47
 
    if hrs > 0:
48
 
        if hrs == 1:
49
 
            seg.append('1 hour')
50
 
        else:
51
 
            seg.append('%d hours' % hrs)
52
 
    if delta.days == 0:
53
 
        if mins > 0:
54
 
            if mins == 1:
55
 
                seg.append('1 minute')
56
 
            else:
57
 
                seg.append('%d minutes' % mins)
58
 
        elif hrs == 0:
59
 
            seg.append('less than a minute')
60
 
    return ', '.join(seg)
61
 
 
62
 
 
63
 
def ago(timestamp):
64
 
    now = datetime.datetime.now()
65
 
    return timespan(now - timestamp) + ' ago'
66
 
    
 
37
# Display of times.
 
38
 
 
39
# date_day -- just the day
 
40
# date_time -- full date with time
 
41
#
 
42
# displaydate -- for use in sentences
 
43
# approximatedate -- for use in tables
 
44
#
 
45
# displaydate and approximatedate return an elementtree <span> Element
 
46
# with the full date in a tooltip.
 
47
 
 
48
def date_day(value):
 
49
    return value.strftime('%Y-%m-%d')
 
50
 
 
51
 
 
52
def date_time(value):
 
53
    return value.strftime('%Y-%m-%d %T')
 
54
 
 
55
 
 
56
def _displaydate(date):
 
57
    delta = abs(datetime.datetime.now() - date)
 
58
    if delta > datetime.timedelta(1, 0, 0):
 
59
        # far in the past or future, display the date
 
60
        return 'on ' + date_day(date)
 
61
    return _approximatedate(date)
 
62
 
 
63
 
 
64
def _approximatedate(date):
 
65
    delta = datetime.datetime.now() - date
 
66
    if abs(delta) > datetime.timedelta(1, 0, 0):
 
67
        # far in the past or future, display the date
 
68
        return date_day(date)
 
69
    future = delta < datetime.timedelta(0, 0, 0)
 
70
    delta = abs(delta)
 
71
    days = delta.days
 
72
    hours = delta.seconds / 3600
 
73
    minutes = (delta.seconds - (3600*hours)) / 60
 
74
    seconds = delta.seconds % 60
 
75
    result = ''
 
76
    if future:
 
77
        result += 'in '
 
78
    if days != 0:
 
79
        amount = days
 
80
        unit = 'day'
 
81
    elif hours != 0:
 
82
        amount = hours
 
83
        unit = 'hour'
 
84
    elif minutes != 0:
 
85
        amount = minutes
 
86
        unit = 'minute'
 
87
    else:
 
88
        amount = seconds
 
89
        unit = 'second'
 
90
    if amount != 1:
 
91
        unit += 's'
 
92
    result += '%s %s' % (amount, unit)
 
93
    if not future:
 
94
        result += ' ago'
 
95
        return result
 
96
 
 
97
 
 
98
def _wrap_with_date_time_title(date, formatted_date):
 
99
    elem = ET.Element("span")
 
100
    elem.text = formatted_date
 
101
    elem.set("title", date_time(date))
 
102
    return elem
 
103
 
 
104
 
 
105
def approximatedate(date):
 
106
    return _wrap_with_date_time_title(date, _approximatedate(date))
 
107
 
 
108
 
 
109
def displaydate(date):
 
110
    return _wrap_with_date_time_title(date, _displaydate(date))
 
111
 
67
112
 
68
113
class Container (object):
69
114
    """
75
120
                setattr(self, key, value)
76
121
        for key, value in kw.iteritems():
77
122
            setattr(self, key, value)
78
 
    
 
123
 
79
124
    def __repr__(self):
80
125
        out = '{ '
81
126
        for key, value in self.__dict__.iteritems():
102
147
    return text[:limit] + '...'
103
148
 
104
149
 
 
150
def to_utf8(s):
 
151
    if isinstance(s, unicode):
 
152
        return s.encode('utf-8')
 
153
    return s
 
154
 
 
155
 
105
156
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
106
157
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
107
158
 
124
175
        return '%s at %s' % (username, domains[-2])
125
176
    return '%s at %s' % (username, domains[0])
126
177
 
127
 
    
 
178
 
128
179
def triple_factors(min_value=1):
129
180
    factors = (1, 3)
130
181
    index = 0
142
193
    """
143
194
    given a position in a maximum range, return a list of negative and positive
144
195
    jump factors for an hgweb-style triple-factor geometric scan.
145
 
    
 
196
 
146
197
    for example, with pos=20 and max=500, the range would be:
147
198
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
148
 
    
 
199
 
149
200
    i admit this is a very strange way of jumping through revisions.  i didn't
150
201
    invent it. :)
151
202
    """
159
210
            out.insert(0, -n)
160
211
 
161
212
 
 
213
# only do this if unicode turns out to be a problem
 
214
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
 
215
 
 
216
# FIXME: get rid of this method; use fixed_width() and avoid XML().
162
217
def html_clean(s):
163
218
    """
164
219
    clean up a string for html display.  expand any tabs, encode any html
165
220
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
166
221
    in displaying monospace text.
167
222
    """
168
 
    s = cgi.escape(s.expandtabs()).replace(' ', '&nbsp;')
 
223
    s = cgi.escape(s.expandtabs())
 
224
    s = s.replace(' ', '&nbsp;')
169
225
    return s
170
226
 
171
227
 
 
228
 
 
229
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
 
230
 
 
231
def fixed_width(s):
 
232
    """
 
233
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
 
234
    chop up the string.
 
235
    """
 
236
    if not isinstance(s, unicode):
 
237
        # this kinda sucks.  file contents are just binary data, and no
 
238
        # encoding metadata is stored, so we need to guess.  this is probably
 
239
        # okay for most code, but for people using things like KOI-8, this
 
240
        # will display gibberish.  we have no way of detecting the correct
 
241
        # encoding to use.
 
242
        try:
 
243
            s = s.decode('utf-8')
 
244
        except UnicodeDecodeError:
 
245
            s = s.decode('iso-8859-15')
 
246
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
247
 
 
248
 
172
249
def fake_permissions(kind, executable):
173
250
    # fake up unix-style permissions given only a "kind" and executable bit
174
251
    if kind == 'directory':
187
264
    return format % value
188
265
 
189
266
 
190
 
def fill_in_navigation(history, navigation):
 
267
def b64(s):
 
268
    s = base64.encodestring(s).replace('\n', '')
 
269
    while (len(s) > 0) and (s[-1] == '='):
 
270
        s = s[:-1]
 
271
    return s
 
272
 
 
273
 
 
274
def uniq(uniqs, s):
 
275
    """
 
276
    turn a potentially long string into a unique smaller string.
 
277
    """
 
278
    if s in uniqs:
 
279
        return uniqs[s]
 
280
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
 
281
    x = struct.pack('>I', next)
 
282
    while (len(x) > 1) and (x[0] == '\x00'):
 
283
        x = x[1:]
 
284
    uniqs[s] = b64(x)
 
285
    return uniqs[s]
 
286
 
 
287
 
 
288
KILO = 1024
 
289
MEG = 1024 * KILO
 
290
GIG = 1024 * MEG
 
291
P95_MEG = int(0.9 * MEG)
 
292
P95_GIG = int(0.9 * GIG)
 
293
 
 
294
def human_size(size, min_divisor=0):
 
295
    size = int(size)
 
296
    if (size == 0) and (min_divisor == 0):
 
297
        return '0'
 
298
    if (size < 512) and (min_divisor == 0):
 
299
        return str(size)
 
300
 
 
301
    if (size >= P95_GIG) or (min_divisor >= GIG):
 
302
        divisor = GIG
 
303
    elif (size >= P95_MEG) or (min_divisor >= MEG):
 
304
        divisor = MEG
 
305
    else:
 
306
        divisor = KILO
 
307
 
 
308
    dot = size % divisor
 
309
    base = size - dot
 
310
    dot = dot * 10 // divisor
 
311
    base //= divisor
 
312
    if dot >= 10:
 
313
        base += 1
 
314
        dot -= 10
 
315
 
 
316
    out = str(base)
 
317
    if (base < 100) and (dot != 0):
 
318
        out += '.%d' % (dot,)
 
319
    if divisor == KILO:
 
320
        out += 'K'
 
321
    elif divisor == MEG:
 
322
        out += 'M'
 
323
    elif divisor == GIG:
 
324
        out += 'G'
 
325
    return out
 
326
 
 
327
 
 
328
def fill_in_navigation(navigation):
191
329
    """
192
330
    given a navigation block (used by the template for the page header), fill
193
331
    in useful calculated values.
194
332
    """
195
 
    navigation.position = history.get_revid_sequence(navigation.revlist, navigation.revid)
196
 
    navigation.count = len(navigation.revlist)
 
333
    if navigation.revid in navigation.revid_list: # XXX is this always true?
 
334
        navigation.position = navigation.revid_list.index(navigation.revid)
 
335
    else:
 
336
        navigation.position = 0
 
337
    navigation.count = len(navigation.revid_list)
197
338
    navigation.page_position = navigation.position // navigation.pagesize + 1
198
 
    navigation.page_count = (len(navigation.revlist) + (navigation.pagesize - 1)) // navigation.pagesize
199
 
    
 
339
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
340
 
200
341
    def get_offset(offset):
201
342
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
202
343
            return None
203
 
        return navigation.revlist[navigation.position + offset]
204
 
    
 
344
        return navigation.revid_list[navigation.position + offset]
 
345
 
205
346
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
206
347
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
 
348
 
 
349
    params = { 'filter_file_id': navigation.filter_file_id }
 
350
    if getattr(navigation, 'query', None) is not None:
 
351
        params['q'] = navigation.query
 
352
    else:
 
353
        params['start_revid'] = navigation.start_revid
 
354
 
207
355
    if navigation.prev_page_revid:
208
 
        navigation.prev_page_url = turbogears.url([ navigation.scan_url, navigation.prev_page_revid ], path=navigation.path, start_revid=navigation.start_revid)
 
356
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
209
357
    if navigation.next_page_revid:
210
 
        navigation.next_page_url = turbogears.url([ navigation.scan_url, navigation.next_page_revid ], path=navigation.path, start_revid=navigation.start_revid)
211
 
 
212
 
 
213
 
# global branch history & cache
214
 
 
215
 
_history = None
216
 
_history_lock = threading.Lock()
217
 
 
218
 
def get_history():
219
 
    global _history
220
 
    from loggerhead.history import History
221
 
    
222
 
    config = get_config()
223
 
    
224
 
    _history_lock.acquire()
225
 
    try:
226
 
        if (_history is None) or _history.out_of_date():
227
 
            log.debug('Reload branch history...')
228
 
            if _history is not None:
229
 
                _history.dont_use_cache()
230
 
            _history = History.from_folder(config.get('folder'))
231
 
            _history.use_cache(config.get('cachepath'))
232
 
        return _history
233
 
    finally:
234
 
        _history_lock.release()
235
 
 
236
 
 
237
 
_config = None
238
 
 
239
 
def set_config(config):
240
 
    global _config
241
 
    _config = config
242
 
 
243
 
def get_config():
244
 
    return _config
245
 
 
 
358
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
 
359
 
 
360
 
 
361
def log_exception(log):
 
362
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
363
        log.debug(line)
 
364
 
 
365
 
 
366
def decorator(unbound):
 
367
    def new_decorator(f):
 
368
        g = unbound(f)
 
369
        g.__name__ = f.__name__
 
370
        g.__doc__ = f.__doc__
 
371
        g.__dict__.update(f.__dict__)
 
372
        return g
 
373
    new_decorator.__name__ = unbound.__name__
 
374
    new_decorator.__doc__ = unbound.__doc__
 
375
    new_decorator.__dict__.update(unbound.__dict__)
 
376
    return new_decorator
 
377
 
 
378
 
 
379
# common threading-lock decorator
 
380
def with_lock(lockname, debug_name=None):
 
381
    if debug_name is None:
 
382
        debug_name = lockname
 
383
    @decorator
 
384
    def _decorator(unbound):
 
385
        def locked(self, *args, **kw):
 
386
            getattr(self, lockname).acquire()
 
387
            try:
 
388
                return unbound(self, *args, **kw)
 
389
            finally:
 
390
                getattr(self, lockname).release()
 
391
        return locked
 
392
    return _decorator
 
393
 
 
394
 
 
395
@decorator
 
396
def strip_whitespace(f):
 
397
    def _f(*a, **kw):
 
398
        out = f(*a, **kw)
 
399
        orig_len = len(out)
 
400
        out = re.sub(r'\n\s+', '\n', out)
 
401
        out = re.sub(r'[ \t]+', ' ', out)
 
402
        out = re.sub(r'\s+\n', '\n', out)
 
403
        new_len = len(out)
 
404
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
 
405
                  human_size(orig_len - new_len),
 
406
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
 
407
        return out
 
408
    return _f
 
409
 
 
410
 
 
411
@decorator
 
412
def lsprof(f):
 
413
    def _f(*a, **kw):
 
414
        from loggerhead.lsprof import profile
 
415
        import cPickle
 
416
        z = time.time()
 
417
        ret, stats = profile(f, *a, **kw)
 
418
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
 
419
        stats.sort()
 
420
        stats.freeze()
 
421
        now = time.time()
 
422
        msec = int(now * 1000) % 1000
 
423
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
 
424
        filename = f.__name__ + '-' + timestr + '.lsprof'
 
425
        cPickle.dump(stats, open(filename, 'w'), 2)
 
426
        return ret
 
427
    return _f
 
428
 
 
429
 
 
430
# just thinking out loud here...
 
431
#
 
432
# so, when browsing around, there are 5 pieces of context, most optional:
 
433
#     - current revid
 
434
#         current location along the navigation path (while browsing)
 
435
#     - starting revid (start_revid)
 
436
#         the current beginning of navigation (navigation continues back to
 
437
#         the original revision) -- this defines an 'alternate mainline'
 
438
#         when the user navigates into a branch.
 
439
#     - file_id
 
440
#         the file being looked at
 
441
#     - filter_file_id
 
442
#         if navigating the revisions that touched a file
 
443
#     - q (query)
 
444
#         if navigating the revisions that matched a search query
 
445
#     - remember
 
446
#         a previous revision to remember for future comparisons
 
447
#
 
448
# current revid is given on the url path.  the rest are optional components
 
449
# in the url params.
 
450
#
 
451
# other transient things can be set:
 
452
#     - compare_revid
 
453
#         to compare one revision to another, on /revision only
 
454
#     - sort
 
455
#         for re-ordering an existing page by different sort
 
456
 
 
457
t_context = threading.local()
 
458
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
 
459
          'compare_revid', 'sort')
 
460
 
 
461
 
 
462
def set_context(map):
 
463
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
 
464
 
 
465
 
 
466
def get_context(**overrides):
 
467
    """
 
468
    return a context map that may be overriden by specific values passed in,
 
469
    but only contains keys from the list of valid context keys.
 
470
 
 
471
    if 'clear' is set, only the 'remember' context value will be added, and
 
472
    all other context will be omitted.
 
473
    """
 
474
    map = dict()
 
475
    if overrides.get('clear', False):
 
476
        map['remember'] = t_context.map.get('remember', None)
 
477
    else:
 
478
        map.update(t_context.map)
 
479
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
 
480
    map.update(overrides)
 
481
    return map