~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Michael Hudson
  • Date: 2007-10-29 16:19:30 UTC
  • mto: This revision was merged to the branch mainline in revision 141.
  • Revision ID: michael.hudson@canonical.com-20071029161930-oxqrd4rd8j1oz3hx
add do nothing check target

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
 
# Copyright (C) 2008  Canonical Ltd.
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
4
#
19
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
18
#
21
19
 
22
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
20
import base64
28
21
import cgi
29
22
import datetime
30
23
import logging
31
24
import re
 
25
import sha
32
26
import struct
 
27
import sys
33
28
import threading
34
29
import time
35
 
import sys
36
 
import os
37
 
import subprocess
 
30
import traceback
 
31
 
 
32
import turbogears
 
33
 
38
34
 
39
35
log = logging.getLogger("loggerhead.controllers")
40
36
 
41
37
 
 
38
def timespan(delta):
 
39
    if delta.days > 730:
 
40
        # good grief!
 
41
        return '%d years' % (int(delta.days // 365.25),)
 
42
    if delta.days >= 3:
 
43
        return '%d days' % delta.days
 
44
    seg = []
 
45
    if delta.days > 0:
 
46
        if delta.days == 1:
 
47
            seg.append('1 day')
 
48
        else:
 
49
            seg.append('%d days' % delta.days)
 
50
    hrs = delta.seconds // 3600
 
51
    mins = (delta.seconds % 3600) // 60
 
52
    if hrs > 0:
 
53
        if hrs == 1:
 
54
            seg.append('1 hour')
 
55
        else:
 
56
            seg.append('%d hours' % hrs)
 
57
    if delta.days == 0:
 
58
        if mins > 0:
 
59
            if mins == 1:
 
60
                seg.append('1 minute')
 
61
            else:
 
62
                seg.append('%d minutes' % mins)
 
63
        elif hrs == 0:
 
64
            seg.append('less than a minute')
 
65
    return ', '.join(seg)
 
66
 
 
67
 
 
68
def ago(timestamp):
 
69
    now = datetime.datetime.now()
 
70
    return timespan(now - timestamp) + ' ago'
 
71
 
 
72
 
42
73
def fix_year(year):
43
74
    if year < 70:
44
75
        year += 2000
46
77
        year += 1900
47
78
    return year
48
79
 
49
 
# Display of times.
50
 
 
51
 
# date_day -- just the day
52
 
# date_time -- full date with time
53
 
#
54
 
# displaydate -- for use in sentences
55
 
# approximatedate -- for use in tables
56
 
#
57
 
# displaydate and approximatedate return an elementtree <span> Element
58
 
# with the full date in a tooltip.
59
 
 
60
 
 
61
 
def date_day(value):
62
 
    return value.strftime('%Y-%m-%d')
63
 
 
64
 
 
65
 
def date_time(value):
66
 
    if value is not None:
67
 
        return value.strftime('%Y-%m-%d %T')
68
 
    else:
69
 
        return 'N/A'
70
 
 
71
 
 
72
 
def _displaydate(date):
73
 
    delta = abs(datetime.datetime.now() - date)
74
 
    if delta > datetime.timedelta(1, 0, 0):
75
 
        # far in the past or future, display the date
76
 
        return 'on ' + date_day(date)
77
 
    return _approximatedate(date)
78
 
 
79
 
 
80
 
def _approximatedate(date):
81
 
    delta = datetime.datetime.now() - date
82
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
83
 
        # far in the past or future, display the date
84
 
        return date_day(date)
85
 
    future = delta < datetime.timedelta(0, 0, 0)
86
 
    delta = abs(delta)
87
 
    days = delta.days
88
 
    hours = delta.seconds / 3600
89
 
    minutes = (delta.seconds - (3600*hours)) / 60
90
 
    seconds = delta.seconds % 60
91
 
    result = ''
92
 
    if future:
93
 
        result += 'in '
94
 
    if days != 0:
95
 
        amount = days
96
 
        unit = 'day'
97
 
    elif hours != 0:
98
 
        amount = hours
99
 
        unit = 'hour'
100
 
    elif minutes != 0:
101
 
        amount = minutes
102
 
        unit = 'minute'
103
 
    else:
104
 
        amount = seconds
105
 
        unit = 'second'
106
 
    if amount != 1:
107
 
        unit += 's'
108
 
    result += '%s %s' % (amount, unit)
109
 
    if not future:
110
 
        result += ' ago'
111
 
        return result
112
 
 
113
 
 
114
 
def _wrap_with_date_time_title(date, formatted_date):
115
 
    elem = ET.Element("span")
116
 
    elem.text = formatted_date
117
 
    elem.set("title", date_time(date))
118
 
    return elem
119
 
 
120
 
 
121
 
def approximatedate(date):
122
 
    #FIXME: Returns an object instead of a string
123
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
124
 
 
125
 
 
126
 
def displaydate(date):
127
 
    return _wrap_with_date_time_title(date, _displaydate(date))
128
 
 
129
80
 
130
81
class Container (object):
131
82
    """
132
83
    Convert a dict into an object with attributes.
133
84
    """
134
 
 
135
85
    def __init__(self, _dict=None, **kw):
136
86
        if _dict is not None:
137
87
            for key, value in _dict.iteritems():
138
88
                setattr(self, key, value)
139
89
        for key, value in kw.iteritems():
140
90
            setattr(self, key, value)
141
 
 
 
91
    
142
92
    def __repr__(self):
143
93
        out = '{ '
144
94
        for key, value in self.__dict__.iteritems():
145
 
            if key.startswith('_') or (getattr(self.__dict__[key],
146
 
                                       '__call__', None) is not None):
 
95
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
147
96
                continue
148
97
            out += '%r => %r, ' % (key, value)
149
98
        out += '}'
150
99
        return out
151
100
 
152
101
 
 
102
def clean_revid(revid):
 
103
    if revid == 'missing':
 
104
        return revid
 
105
    return sha.new(revid).hexdigest()
 
106
 
 
107
 
 
108
def obfuscate(text):
 
109
    return ''.join([ '&#%d;' % ord(c) for c in text ])
 
110
 
 
111
 
153
112
def trunc(text, limit=10):
154
113
    if len(text) <= limit:
155
114
        return text
156
115
    return text[:limit] + '...'
157
116
 
158
117
 
 
118
def to_utf8(s):
 
119
    if isinstance(s, unicode):
 
120
        return s.encode('utf-8')
 
121
    return s
 
122
 
 
123
 
159
124
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
160
125
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
161
126
 
162
 
 
163
127
def hide_email(email):
164
128
    """
165
129
    try to obsure any email address in a bazaar committer's name.
179
143
        return '%s at %s' % (username, domains[-2])
180
144
    return '%s at %s' % (username, domains[0])
181
145
 
 
146
    
 
147
def triple_factors(min_value=1):
 
148
    factors = (1, 3)
 
149
    index = 0
 
150
    n = 1
 
151
    while True:
 
152
        if n >= min_value:
 
153
            yield n * factors[index]
 
154
        index += 1
 
155
        if index >= len(factors):
 
156
            index = 0
 
157
            n *= 10
 
158
 
 
159
 
 
160
def scan_range(pos, max, pagesize=1):
 
161
    """
 
162
    given a position in a maximum range, return a list of negative and positive
 
163
    jump factors for an hgweb-style triple-factor geometric scan.
 
164
    
 
165
    for example, with pos=20 and max=500, the range would be:
 
166
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
167
    
 
168
    i admit this is a very strange way of jumping through revisions.  i didn't
 
169
    invent it. :)
 
170
    """
 
171
    out = []
 
172
    for n in triple_factors(pagesize + 1):
 
173
        if n > max:
 
174
            return out
 
175
        if pos + n < max:
 
176
            out.append(n)
 
177
        if pos - n >= 0:
 
178
            out.insert(0, -n)
 
179
 
182
180
 
183
181
# only do this if unicode turns out to be a problem
184
182
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
185
183
 
186
184
# FIXME: get rid of this method; use fixed_width() and avoid XML().
187
 
 
188
 
 
189
185
def html_clean(s):
190
186
    """
191
187
    clean up a string for html display.  expand any tabs, encode any html
197
193
    return s
198
194
 
199
195
 
 
196
 
200
197
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
201
198
 
202
 
 
203
 
def fill_div(s):
204
 
    """
205
 
    CSS is stupid. In some cases we need to replace an empty value with
206
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
207
 
 
208
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
209
 
    """
210
 
 
211
 
 
212
 
    if s is None:
213
 
        return '&nbsp;'
214
 
    elif isinstance(s, int):
215
 
        return s
216
 
    elif not s.strip():
217
 
        return '&nbsp;'
218
 
    else:
219
 
        try:
220
 
            s = s.decode('utf-8')
221
 
        except UnicodeDecodeError:
222
 
            s = s.decode('iso-8859-15')
223
 
        return s
224
 
 
225
 
 
226
199
def fixed_width(s):
227
200
    """
228
201
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
250
223
    return '-rw-r--r--'
251
224
 
252
225
 
 
226
def if_present(format, value):
 
227
    """
 
228
    format a value using a format string, if the value exists and is not None.
 
229
    """
 
230
    if value is None:
 
231
        return ''
 
232
    return format % value
 
233
 
 
234
 
253
235
def b64(s):
254
236
    s = base64.encodestring(s).replace('\n', '')
255
237
    while (len(s) > 0) and (s[-1] == '='):
277
259
P95_MEG = int(0.9 * MEG)
278
260
P95_GIG = int(0.9 * GIG)
279
261
 
280
 
 
281
262
def human_size(size, min_divisor=0):
282
263
    size = int(size)
283
264
    if (size == 0) and (min_divisor == 0):
291
272
        divisor = MEG
292
273
    else:
293
274
        divisor = KILO
294
 
 
 
275
    
295
276
    dot = size % divisor
296
277
    base = size - dot
297
278
    dot = dot * 10 // divisor
299
280
    if dot >= 10:
300
281
        base += 1
301
282
        dot -= 10
302
 
 
 
283
    
303
284
    out = str(base)
304
285
    if (base < 100) and (dot != 0):
305
 
        out += '.%d' % (dot)
 
286
        out += '.%d' % (dot,)
306
287
    if divisor == KILO:
307
288
        out += 'K'
308
289
    elif divisor == MEG:
310
291
    elif divisor == GIG:
311
292
        out += 'G'
312
293
    return out
313
 
 
 
294
    
314
295
 
315
296
def fill_in_navigation(navigation):
316
297
    """
323
304
        navigation.position = 0
324
305
    navigation.count = len(navigation.revid_list)
325
306
    navigation.page_position = navigation.position // navigation.pagesize + 1
326
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
327
 
 - 1)) // navigation.pagesize
328
 
 
 
307
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
308
    
329
309
    def get_offset(offset):
330
 
        if (navigation.position + offset < 0) or (
331
 
           navigation.position + offset > navigation.count - 1):
 
310
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
332
311
            return None
333
312
        return navigation.revid_list[navigation.position + offset]
334
 
 
335
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
 
313
    
336
314
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
337
315
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
338
 
    prev_page_revno = navigation.history.get_revno(
339
 
            navigation.prev_page_revid)
340
 
    next_page_revno = navigation.history.get_revno(
341
 
            navigation.next_page_revid)
342
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
343
 
 
344
 
    params = {'filter_file_id': navigation.filter_file_id}
 
316
    
 
317
    params = { 'file_id': navigation.file_id }
345
318
    if getattr(navigation, 'query', None) is not None:
346
319
        params['q'] = navigation.query
347
 
 
348
 
    if getattr(navigation, 'start_revid', None) is not None:
349
 
        params['start_revid'] = start_revno
350
 
 
 
320
    else:
 
321
        params['start_revid'] = navigation.start_revid
 
322
        
351
323
    if navigation.prev_page_revid:
352
 
        navigation.prev_page_url = navigation.branch.context_url(
353
 
            [navigation.scan_url, prev_page_revno], **params)
 
324
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
354
325
    if navigation.next_page_revid:
355
 
        navigation.next_page_url = navigation.branch.context_url(
356
 
            [navigation.scan_url, next_page_revno], **params)
357
 
 
358
 
 
359
 
def directory_breadcrumbs(path, is_root, view):
360
 
    """
361
 
    Generate breadcrumb information from the directory path given
362
 
 
363
 
    The path given should be a path up to any branch that is currently being
364
 
    served
365
 
 
366
 
    Arguments:
367
 
    path -- The path to convert into breadcrumbs
368
 
    is_root -- Whether or not loggerhead is serving a branch at its root
369
 
    view -- The type of view we are showing (files, changes etc)
370
 
    """
371
 
    # Is our root directory itself a branch?
372
 
    if is_root:
373
 
        if view == 'directory':
374
 
            directory = 'files'
375
 
        breadcrumbs = [{
376
 
            'dir_name': path,
377
 
            'path': '',
378
 
            'suffix': view,
379
 
        }]
380
 
    else:
381
 
        # Create breadcrumb trail for the path leading up to the branch
382
 
        breadcrumbs = [{
383
 
            'dir_name': "(root)",
384
 
            'path': '',
385
 
            'suffix': '',
386
 
        }]
387
 
        if path != '/':
388
 
            dir_parts = path.strip('/').split('/')
389
 
            for index, dir_name in enumerate(dir_parts):
390
 
                breadcrumbs.append({
391
 
                    'dir_name': dir_name,
392
 
                    'path': '/'.join(dir_parts[:index + 1]),
393
 
                    'suffix': '',
394
 
                })
395
 
            # If we are not in the directory view, the last crumb is a branch,
396
 
            # so we need to specify a view
397
 
            if view != 'directory':
398
 
                breadcrumbs[-1]['suffix'] = '/' + view
399
 
    return breadcrumbs
400
 
 
401
 
 
402
 
def branch_breadcrumbs(path, inv, view):
403
 
    """
404
 
    Generate breadcrumb information from the branch path given
405
 
 
406
 
    The path given should be a path that exists within a branch
407
 
 
408
 
    Arguments:
409
 
    path -- The path to convert into breadcrumbs
410
 
    inv -- Inventory to get file information from
411
 
    view -- The type of view we are showing (files, changes etc)
412
 
    """
413
 
    dir_parts = path.strip('/').split('/')
414
 
    inner_breadcrumbs = []
415
 
    for index, dir_name in enumerate(dir_parts):
416
 
        inner_breadcrumbs.append({
417
 
            'dir_name': dir_name,
418
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
419
 
            'suffix': '/' + view,
420
 
        })
421
 
    return inner_breadcrumbs
 
326
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
 
327
 
 
328
 
 
329
def log_exception(log):
 
330
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
331
        log.debug(line)
422
332
 
423
333
 
424
334
def decorator(unbound):
425
 
 
426
335
    def new_decorator(f):
427
336
        g = unbound(f)
428
337
        g.__name__ = f.__name__
436
345
 
437
346
 
438
347
# common threading-lock decorator
439
 
 
440
 
 
441
348
def with_lock(lockname, debug_name=None):
442
349
    if debug_name is None:
443
350
        debug_name = lockname
444
 
 
445
351
    @decorator
446
352
    def _decorator(unbound):
447
 
 
448
353
        def locked(self, *args, **kw):
449
354
            getattr(self, lockname).acquire()
450
355
            try:
456
361
 
457
362
 
458
363
@decorator
 
364
def strip_whitespace(f):
 
365
    def _f(*a, **kw):
 
366
        out = f(*a, **kw)
 
367
        orig_len = len(out)
 
368
        out = re.sub(r'\n\s+', '\n', out)
 
369
        out = re.sub(r'[ \t]+', ' ', out)
 
370
        out = re.sub(r'\s+\n', '\n', out)
 
371
        new_len = len(out)
 
372
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
 
373
                  human_size(orig_len - new_len),
 
374
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
 
375
        return out
 
376
    return _f
 
377
 
 
378
 
 
379
@decorator
459
380
def lsprof(f):
460
 
 
461
381
    def _f(*a, **kw):
462
382
        from loggerhead.lsprof import profile
463
383
        import cPickle
464
384
        z = time.time()
465
385
        ret, stats = profile(f, *a, **kw)
466
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
467
 
            int((time.time() - z) * 1000)))
 
386
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
468
387
        stats.sort()
469
388
        stats.freeze()
470
389
        now = time.time()
471
390
        msec = int(now * 1000) % 1000
472
 
        timestr = time.strftime('%Y%m%d%H%M%S',
473
 
                                time.localtime(now)) + ('%03d' % msec)
 
391
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
474
392
        filename = f.__name__ + '-' + timestr + '.lsprof'
475
393
        cPickle.dump(stats, open(filename, 'w'), 2)
476
394
        return ret
484
402
#         current location along the navigation path (while browsing)
485
403
#     - starting revid (start_revid)
486
404
#         the current beginning of navigation (navigation continues back to
487
 
#         the original revision) -- this defines an 'alternate mainline'
488
 
#         when the user navigates into a branch.
 
405
#         the original revision) -- this may not be along the primary revision
 
406
#         path since the user may have navigated into a branch
489
407
#     - file_id
490
 
#         the file being looked at
491
 
#     - filter_file_id
492
408
#         if navigating the revisions that touched a file
493
409
#     - q (query)
494
410
#         if navigating the revisions that matched a search query
505
421
#         for re-ordering an existing page by different sort
506
422
 
507
423
t_context = threading.local()
508
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
509
 
          'compare_revid', 'sort')
 
424
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
510
425
 
511
426
 
512
427
def set_context(map):
515
430
 
516
431
def get_context(**overrides):
517
432
    """
518
 
    Soon to be deprecated.
519
 
 
520
 
 
521
433
    return a context map that may be overriden by specific values passed in,
522
434
    but only contains keys from the list of valid context keys.
523
 
 
 
435
    
524
436
    if 'clear' is set, only the 'remember' context value will be added, and
525
437
    all other context will be omitted.
526
438
    """
532
444
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
533
445
    map.update(overrides)
534
446
    return map
535
 
 
536
 
 
537
 
class Reloader(object):
538
 
    """
539
 
    This class wraps all paste.reloader logic. All methods are @classmethod.
540
 
    """
541
 
 
542
 
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
543
 
 
544
 
    @classmethod
545
 
    def _turn_sigterm_into_systemexit(self):
546
 
        """
547
 
        Attempts to turn a SIGTERM exception into a SystemExit exception.
548
 
        """
549
 
        try:
550
 
            import signal
551
 
        except ImportError:
552
 
            return
553
 
 
554
 
        def handle_term(signo, frame):
555
 
            raise SystemExit
556
 
        signal.signal(signal.SIGTERM, handle_term)
557
 
 
558
 
    @classmethod
559
 
    def is_installed(self):
560
 
        return os.environ.get(self._reloader_environ_key)
561
 
 
562
 
    @classmethod
563
 
    def install(self):
564
 
        from paste import reloader
565
 
        reloader.install(int(1))
566
 
 
567
 
    @classmethod
568
 
    def restart_with_reloader(self):
569
 
        """Based on restart_with_monitor from paste.script.serve."""
570
 
        print 'Starting subprocess with file monitor'
571
 
        while 1:
572
 
            args = [sys.executable] + sys.argv
573
 
            new_environ = os.environ.copy()
574
 
            new_environ[self._reloader_environ_key] = 'true'
575
 
            proc = None
576
 
            try:
577
 
                try:
578
 
                    self._turn_sigterm_into_systemexit()
579
 
                    proc = subprocess.Popen(args, env=new_environ)
580
 
                    exit_code = proc.wait()
581
 
                    proc = None
582
 
                except KeyboardInterrupt:
583
 
                    print '^C caught in monitor process'
584
 
                    return 1
585
 
            finally:
586
 
                if (proc is not None
587
 
                    and hasattr(os, 'kill')):
588
 
                    import signal
589
 
                    try:
590
 
                        os.kill(proc.pid, signal.SIGTERM)
591
 
                    except (OSError, IOError):
592
 
                        pass
593
 
 
594
 
            # Reloader always exits with code 3; but if we are
595
 
            # a monitor, any exit code will restart
596
 
            if exit_code != 3:
597
 
                return exit_code
598
 
            print '-'*20, 'Restarting', '-'*20