~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
 
# Copyright (C) 2008  Canonical Ltd.
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
 
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
3
#
7
4
# This program is free software; you can redistribute it and/or modify
8
5
# it under the terms of the GNU General Public License as published by
19
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
17
#
21
18
 
22
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
 
from simpletal.simpleTALUtils import HTMLStructureCleaner
28
 
 
29
 
import base64
30
 
import cgi
31
 
import datetime
32
 
import logging
33
19
import re
34
 
import struct
35
 
import threading
36
 
import time
37
 
import sys
38
 
import os
39
 
import subprocess
40
 
 
41
 
log = logging.getLogger("loggerhead.controllers")
42
 
 
43
 
 
44
 
def fix_year(year):
45
 
    if year < 70:
46
 
        year += 2000
47
 
    if year < 100:
48
 
        year += 1900
49
 
    return year
50
 
 
51
 
# Display of times.
52
 
 
53
 
# date_day -- just the day
54
 
# date_time -- full date with time
55
 
#
56
 
# displaydate -- for use in sentences
57
 
# approximatedate -- for use in tables
58
 
#
59
 
# displaydate and approximatedate return an elementtree <span> Element
60
 
# with the full date in a tooltip.
61
 
 
62
 
 
63
 
def date_day(value):
64
 
    return value.strftime('%Y-%m-%d')
65
 
 
66
 
 
67
 
def date_time(value):
68
 
    if value is not None:
69
 
        return value.strftime('%Y-%m-%d %T')
70
 
    else:
71
 
        return 'N/A'
72
 
 
73
 
 
74
 
def _displaydate(date):
75
 
    delta = abs(datetime.datetime.now() - date)
76
 
    if delta > datetime.timedelta(1, 0, 0):
77
 
        # far in the past or future, display the date
78
 
        return 'on ' + date_day(date)
79
 
    return _approximatedate(date)
80
 
 
81
 
 
82
 
def _approximatedate(date):
83
 
    delta = datetime.datetime.now() - date
84
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
85
 
        # far in the past or future, display the date
86
 
        return date_day(date)
87
 
    future = delta < datetime.timedelta(0, 0, 0)
88
 
    delta = abs(delta)
89
 
    days = delta.days
90
 
    hours = delta.seconds / 3600
91
 
    minutes = (delta.seconds - (3600*hours)) / 60
92
 
    seconds = delta.seconds % 60
93
 
    result = ''
94
 
    if future:
95
 
        result += 'in '
96
 
    if days != 0:
97
 
        amount = days
98
 
        unit = 'day'
99
 
    elif hours != 0:
100
 
        amount = hours
101
 
        unit = 'hour'
102
 
    elif minutes != 0:
103
 
        amount = minutes
104
 
        unit = 'minute'
105
 
    else:
106
 
        amount = seconds
107
 
        unit = 'second'
108
 
    if amount != 1:
109
 
        unit += 's'
110
 
    result += '%s %s' % (amount, unit)
111
 
    if not future:
112
 
        result += ' ago'
113
 
        return result
114
 
 
115
 
 
116
 
def _wrap_with_date_time_title(date, formatted_date):
117
 
    elem = ET.Element("span")
118
 
    elem.text = formatted_date
119
 
    elem.set("title", date_time(date))
120
 
    return elem
121
 
 
122
 
 
123
 
def approximatedate(date):
124
 
    #FIXME: Returns an object instead of a string
125
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
126
 
 
127
 
 
128
 
def displaydate(date):
129
 
    return _wrap_with_date_time_title(date, _displaydate(date))
 
20
import sha
 
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)
130
48
 
131
49
 
132
50
class Container (object):
133
51
    """
134
52
    Convert a dict into an object with attributes.
135
53
    """
136
 
 
137
54
    def __init__(self, _dict=None, **kw):
138
55
        if _dict is not None:
139
56
            for key, value in _dict.iteritems():
141
58
        for key, value in kw.iteritems():
142
59
            setattr(self, key, value)
143
60
 
144
 
    def __repr__(self):
145
 
        out = '{ '
146
 
        for key, value in self.__dict__.iteritems():
147
 
            if key.startswith('_') or (getattr(self.__dict__[key],
148
 
                                       '__call__', None) is not None):
149
 
                continue
150
 
            out += '%r => %r, ' % (key, value)
151
 
        out += '}'
152
 
        return out
153
 
 
154
 
 
155
 
def trunc(text, limit=10):
156
 
    if len(text) <= limit:
157
 
        return text
158
 
    return text[:limit] + '...'
 
61
 
 
62
def clean_revid(revid):
 
63
    if revid == 'missing':
 
64
        return revid
 
65
    return sha.new(revid).hexdigest()
 
66
 
 
67
 
 
68
def obfuscate(text):
 
69
    return ''.join([ '&#%d;' % ord(c) for c in text ])
159
70
 
160
71
 
161
72
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
162
73
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
163
74
 
164
 
 
165
75
def hide_email(email):
166
76
    """
167
77
    try to obsure any email address in a bazaar committer's name.
181
91
        return '%s at %s' % (username, domains[-2])
182
92
    return '%s at %s' % (username, domains[0])
183
93
 
184
 
def hide_emails(emails):
185
 
    """
186
 
    try to obscure any email address in a list of bazaar committers' names.
187
 
    """
188
 
    result = []
189
 
    for email in emails:
190
 
        result.append(hide_email(email))
191
 
    return result
192
 
 
193
 
# only do this if unicode turns out to be a problem
194
 
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
195
 
 
196
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
197
 
 
198
 
 
199
 
def html_clean(s):
200
 
    """
201
 
    clean up a string for html display.  expand any tabs, encode any html
202
 
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
203
 
    in displaying monospace text.
204
 
    """
205
 
    s = cgi.escape(s.expandtabs())
206
 
    s = s.replace(' ', '&nbsp;')
207
 
    return s
208
 
 
209
 
 
210
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
211
 
 
212
 
 
213
 
def fill_div(s):
214
 
    """
215
 
    CSS is stupid. In some cases we need to replace an empty value with
216
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
217
 
 
218
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
219
 
    """
220
 
 
221
 
 
222
 
    if s is None:
223
 
        return '&nbsp;'
224
 
    elif isinstance(s, int):
225
 
        return s
226
 
    elif not s.strip():
227
 
        return '&nbsp;'
228
 
    else:
229
 
        try:
230
 
            s = s.decode('utf-8')
231
 
        except UnicodeDecodeError:
232
 
            s = s.decode('iso-8859-15')
233
 
        return s
234
 
 
235
 
HSC = HTMLStructureCleaner()
236
 
 
237
 
def fixed_width(s):
238
 
    """
239
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
240
 
    chop up the string.
241
 
    """
242
 
    if not isinstance(s, unicode):
243
 
        # this kinda sucks.  file contents are just binary data, and no
244
 
        # encoding metadata is stored, so we need to guess.  this is probably
245
 
        # okay for most code, but for people using things like KOI-8, this
246
 
        # will display gibberish.  we have no way of detecting the correct
247
 
        # encoding to use.
248
 
        try:
249
 
            s = s.decode('utf-8')
250
 
        except UnicodeDecodeError:
251
 
            s = s.decode('iso-8859-15')
252
 
 
253
 
    s = cgi.escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
254
 
 
255
 
    return HSC.clean(s).replace('\n', '<br/>')
256
 
 
257
 
 
258
 
def fake_permissions(kind, executable):
259
 
    # fake up unix-style permissions given only a "kind" and executable bit
260
 
    if kind == 'directory':
261
 
        return 'drwxr-xr-x'
262
 
    if executable:
263
 
        return '-rwxr-xr-x'
264
 
    return '-rw-r--r--'
265
 
 
266
 
 
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
 
 
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\
341
 
 - 1)) // navigation.pagesize
342
 
 
343
 
    def get_offset(offset):
344
 
        if (navigation.position + offset < 0) or (
345
 
           navigation.position + offset > navigation.count - 1):
346
 
            return None
347
 
        return navigation.revid_list[navigation.position + offset]
348
 
 
349
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
350
 
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
351
 
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
352
 
    prev_page_revno = navigation.history.get_revno(
353
 
            navigation.prev_page_revid)
354
 
    next_page_revno = navigation.history.get_revno(
355
 
            navigation.next_page_revid)
356
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
357
 
 
358
 
    params = {'filter_file_id': navigation.filter_file_id}
359
 
    if getattr(navigation, 'query', None) is not None:
360
 
        params['q'] = navigation.query
361
 
 
362
 
    if getattr(navigation, 'start_revid', None) is not None:
363
 
        params['start_revid'] = start_revno
364
 
 
365
 
    if navigation.prev_page_revid:
366
 
        navigation.prev_page_url = navigation.branch.context_url(
367
 
            [navigation.scan_url, prev_page_revno], **params)
368
 
    if navigation.next_page_revid:
369
 
        navigation.next_page_url = navigation.branch.context_url(
370
 
            [navigation.scan_url, next_page_revno], **params)
371
 
 
372
 
 
373
 
def directory_breadcrumbs(path, is_root, view):
374
 
    """
375
 
    Generate breadcrumb information from the directory path given
376
 
 
377
 
    The path given should be a path up to any branch that is currently being
378
 
    served
379
 
 
380
 
    Arguments:
381
 
    path -- The path to convert into breadcrumbs
382
 
    is_root -- Whether or not loggerhead is serving a branch at its root
383
 
    view -- The type of view we are showing (files, changes etc)
384
 
    """
385
 
    # Is our root directory itself a branch?
386
 
    if is_root:
387
 
        if view == 'directory':
388
 
            directory = 'files'
389
 
        breadcrumbs = [{
390
 
            'dir_name': path,
391
 
            'path': '',
392
 
            'suffix': view,
393
 
        }]
394
 
    else:
395
 
        # Create breadcrumb trail for the path leading up to the branch
396
 
        breadcrumbs = [{
397
 
            'dir_name': "(root)",
398
 
            'path': '',
399
 
            'suffix': '',
400
 
        }]
401
 
        if path != '/':
402
 
            dir_parts = path.strip('/').split('/')
403
 
            for index, dir_name in enumerate(dir_parts):
404
 
                breadcrumbs.append({
405
 
                    'dir_name': dir_name,
406
 
                    'path': '/'.join(dir_parts[:index + 1]),
407
 
                    'suffix': '',
408
 
                })
409
 
            # If we are not in the directory view, the last crumb is a branch,
410
 
            # so we need to specify a view
411
 
            if view != 'directory':
412
 
                breadcrumbs[-1]['suffix'] = '/' + view
413
 
    return breadcrumbs
414
 
 
415
 
 
416
 
def branch_breadcrumbs(path, inv, view):
417
 
    """
418
 
    Generate breadcrumb information from the branch path given
419
 
 
420
 
    The path given should be a path that exists within a branch
421
 
 
422
 
    Arguments:
423
 
    path -- The path to convert into breadcrumbs
424
 
    inv -- Inventory to get file information from
425
 
    view -- The type of view we are showing (files, changes etc)
426
 
    """
427
 
    dir_parts = path.strip('/').split('/')
428
 
    inner_breadcrumbs = []
429
 
    for index, dir_name in enumerate(dir_parts):
430
 
        inner_breadcrumbs.append({
431
 
            'dir_name': dir_name,
432
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
433
 
            'suffix': '/' + view,
434
 
        })
435
 
    return inner_breadcrumbs
436
 
 
437
 
 
438
 
def decorator(unbound):
439
 
 
440
 
    def new_decorator(f):
441
 
        g = unbound(f)
442
 
        g.__name__ = f.__name__
443
 
        g.__doc__ = f.__doc__
444
 
        g.__dict__.update(f.__dict__)
445
 
        return g
446
 
    new_decorator.__name__ = unbound.__name__
447
 
    new_decorator.__doc__ = unbound.__doc__
448
 
    new_decorator.__dict__.update(unbound.__dict__)
449
 
    return new_decorator
450
 
 
451
 
 
452
 
 
453
 
@decorator
454
 
def lsprof(f):
455
 
 
456
 
    def _f(*a, **kw):
457
 
        from loggerhead.lsprof import profile
458
 
        import cPickle
459
 
        z = time.time()
460
 
        ret, stats = profile(f, *a, **kw)
461
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
462
 
            int((time.time() - z) * 1000)))
463
 
        stats.sort()
464
 
        stats.freeze()
465
 
        now = time.time()
466
 
        msec = int(now * 1000) % 1000
467
 
        timestr = time.strftime('%Y%m%d%H%M%S',
468
 
                                time.localtime(now)) + ('%03d' % msec)
469
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
470
 
        cPickle.dump(stats, open(filename, 'w'), 2)
471
 
        return ret
472
 
    return _f
473
 
 
474
 
 
475
 
# just thinking out loud here...
476
 
#
477
 
# so, when browsing around, there are 5 pieces of context, most optional:
478
 
#     - current revid
479
 
#         current location along the navigation path (while browsing)
480
 
#     - starting revid (start_revid)
481
 
#         the current beginning of navigation (navigation continues back to
482
 
#         the original revision) -- this defines an 'alternate mainline'
483
 
#         when the user navigates into a branch.
484
 
#     - file_id
485
 
#         the file being looked at
486
 
#     - filter_file_id
487
 
#         if navigating the revisions that touched a file
488
 
#     - q (query)
489
 
#         if navigating the revisions that matched a search query
490
 
#     - remember
491
 
#         a previous revision to remember for future comparisons
492
 
#
493
 
# current revid is given on the url path.  the rest are optional components
494
 
# in the url params.
495
 
#
496
 
# other transient things can be set:
497
 
#     - compare_revid
498
 
#         to compare one revision to another, on /revision only
499
 
#     - sort
500
 
#         for re-ordering an existing page by different sort
501
 
 
502
 
t_context = threading.local()
503
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
504
 
          'compare_revid', 'sort')
505
 
 
506
 
 
507
 
def set_context(map):
508
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
509
 
 
510
 
 
511
 
def get_context(**overrides):
512
 
    """
513
 
    Soon to be deprecated.
514
 
 
515
 
 
516
 
    return a context map that may be overriden by specific values passed in,
517
 
    but only contains keys from the list of valid context keys.
518
 
 
519
 
    if 'clear' is set, only the 'remember' context value will be added, and
520
 
    all other context will be omitted.
521
 
    """
522
 
    map = dict()
523
 
    if overrides.get('clear', False):
524
 
        map['remember'] = t_context.map.get('remember', None)
525
 
    else:
526
 
        map.update(t_context.map)
527
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
528
 
    map.update(overrides)
529
 
    return map
530
 
 
531
 
 
532
 
class Reloader(object):
533
 
    """
534
 
    This class wraps all paste.reloader logic. All methods are @classmethod.
535
 
    """
536
 
 
537
 
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
538
 
 
539
 
    @classmethod
540
 
    def _turn_sigterm_into_systemexit(self):
541
 
        """
542
 
        Attempts to turn a SIGTERM exception into a SystemExit exception.
543
 
        """
544
 
        try:
545
 
            import signal
546
 
        except ImportError:
547
 
            return
548
 
 
549
 
        def handle_term(signo, frame):
550
 
            raise SystemExit
551
 
        signal.signal(signal.SIGTERM, handle_term)
552
 
 
553
 
    @classmethod
554
 
    def is_installed(self):
555
 
        return os.environ.get(self._reloader_environ_key)
556
 
 
557
 
    @classmethod
558
 
    def install(self):
559
 
        from paste import reloader
560
 
        reloader.install(int(1))
561
 
 
562
 
    @classmethod
563
 
    def restart_with_reloader(self):
564
 
        """Based on restart_with_monitor from paste.script.serve."""
565
 
        print 'Starting subprocess with file monitor'
566
 
        while 1:
567
 
            args = [sys.executable] + sys.argv
568
 
            new_environ = os.environ.copy()
569
 
            new_environ[self._reloader_environ_key] = 'true'
570
 
            proc = None
571
 
            try:
572
 
                try:
573
 
                    self._turn_sigterm_into_systemexit()
574
 
                    proc = subprocess.Popen(args, env=new_environ)
575
 
                    exit_code = proc.wait()
576
 
                    proc = None
577
 
                except KeyboardInterrupt:
578
 
                    print '^C caught in monitor process'
579
 
                    return 1
580
 
            finally:
581
 
                if (proc is not None
582
 
                    and hasattr(os, 'kill')):
583
 
                    import signal
584
 
                    try:
585
 
                        os.kill(proc.pid, signal.SIGTERM)
586
 
                    except (OSError, IOError):
587
 
                        pass
588
 
 
589
 
            # Reloader always exits with code 3; but if we are
590
 
            # a monitor, any exit code will restart
591
 
            if exit_code != 3:
592
 
                return exit_code
593
 
            print '-'*20, 'Restarting', '-'*20
 
94
    
 
95
def triple_factors():
 
96
    factors = (1, 3)
 
97
    index = 0
 
98
    n = 1
 
99
    while True:
 
100
        yield n * factors[index]
 
101
        index += 1
 
102
        if index >= len(factors):
 
103
            index = 0
 
104
            n *= 10
 
105
 
 
106
 
 
107
def scan_range(pos, max):
 
108
    """
 
109
    given a position in a maximum range, return a list of negative and positive
 
110
    jump factors for an hgweb-style triple-factor geometric scan.
 
111
    
 
112
    for example, with pos=20 and max=500, the range would be:
 
113
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
114
    
 
115
    i admit this is a very strange way of jumping through revisions.  i didn't
 
116
    invent it. :)
 
117
    """
 
118
    out = []
 
119
    for n in triple_factors():
 
120
        if n > max:
 
121
            return out
 
122
        if pos + n < max:
 
123
            out.append(n)
 
124
        if pos - n >= 0:
 
125
            out.insert(0, -n)
 
126