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