~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Marius Kruger
  • Date: 2008-10-02 21:12:58 UTC
  • mto: This revision was merged to the branch mainline in revision 240.
  • Revision ID: amanic@gmail.com-20081002211258-n28j8aa8fmp3u5ck
Add installation instructions for loggerheadd to README.txt and changed ubuntu -> Ubuntu for consistancy.

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