~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robey Pointer
  • Date: 2006-12-11 06:44:19 UTC
  • Revision ID: robey@lag.net-20061211064419-8ssa7mlsiflpmy0c
initial checkin

Show diffs side-by-side

added added

removed removed

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