~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
 
import base64
28
 
import cgi
29
 
import datetime
30
 
import logging
31
19
import re
32
 
import struct
33
 
import threading
34
 
import time
35
 
import sys
36
 
import os
37
 
import subprocess
38
 
 
39
 
log = logging.getLogger("loggerhead.controllers")
40
 
 
41
 
 
42
 
def fix_year(year):
43
 
    if year < 70:
44
 
        year += 2000
45
 
    if year < 100:
46
 
        year += 1900
47
 
    return year
48
 
 
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))
 
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)
128
48
 
129
49
 
130
50
class Container (object):
131
51
    """
132
52
    Convert a dict into an object with attributes.
133
53
    """
134
 
 
135
54
    def __init__(self, _dict=None, **kw):
136
55
        if _dict is not None:
137
56
            for key, value in _dict.iteritems():
139
58
        for key, value in kw.iteritems():
140
59
            setattr(self, key, value)
141
60
 
142
 
    def __repr__(self):
143
 
        out = '{ '
144
 
        for key, value in self.__dict__.iteritems():
145
 
            if key.startswith('_') or (getattr(self.__dict__[key],
146
 
                                       '__call__', None) is not None):
147
 
                continue
148
 
            out += '%r => %r, ' % (key, value)
149
 
        out += '}'
150
 
        return out
151
 
 
152
 
 
153
 
def trunc(text, limit=10):
154
 
    if len(text) <= limit:
155
 
        return text
156
 
    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 ])
157
70
 
158
71
 
159
72
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
160
73
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
161
74
 
162
 
 
163
75
def hide_email(email):
164
76
    """
165
77
    try to obsure any email address in a bazaar committer's name.
179
91
        return '%s at %s' % (username, domains[-2])
180
92
    return '%s at %s' % (username, domains[0])
181
93
 
182
 
 
183
 
# only do this if unicode turns out to be a problem
184
 
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
185
 
 
186
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
187
 
 
188
 
 
189
 
def html_clean(s):
190
 
    """
191
 
    clean up a string for html display.  expand any tabs, encode any html
192
 
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
193
 
    in displaying monospace text.
194
 
    """
195
 
    s = cgi.escape(s.expandtabs())
196
 
    s = s.replace(' ', '&nbsp;')
197
 
    return s
198
 
 
199
 
 
200
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
201
 
 
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
 
def fixed_width(s):
227
 
    """
228
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
229
 
    chop up the string.
230
 
    """
231
 
    if not isinstance(s, unicode):
232
 
        # this kinda sucks.  file contents are just binary data, and no
233
 
        # encoding metadata is stored, so we need to guess.  this is probably
234
 
        # okay for most code, but for people using things like KOI-8, this
235
 
        # will display gibberish.  we have no way of detecting the correct
236
 
        # encoding to use.
237
 
        try:
238
 
            s = s.decode('utf-8')
239
 
        except UnicodeDecodeError:
240
 
            s = s.decode('iso-8859-15')
241
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
242
 
 
243
 
 
244
 
def fake_permissions(kind, executable):
245
 
    # fake up unix-style permissions given only a "kind" and executable bit
246
 
    if kind == 'directory':
247
 
        return 'drwxr-xr-x'
248
 
    if executable:
249
 
        return '-rwxr-xr-x'
250
 
    return '-rw-r--r--'
251
 
 
252
 
 
253
 
def b64(s):
254
 
    s = base64.encodestring(s).replace('\n', '')
255
 
    while (len(s) > 0) and (s[-1] == '='):
256
 
        s = s[:-1]
257
 
    return s
258
 
 
259
 
 
260
 
def uniq(uniqs, s):
261
 
    """
262
 
    turn a potentially long string into a unique smaller string.
263
 
    """
264
 
    if s in uniqs:
265
 
        return uniqs[s]
266
 
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
267
 
    x = struct.pack('>I', next)
268
 
    while (len(x) > 1) and (x[0] == '\x00'):
269
 
        x = x[1:]
270
 
    uniqs[s] = b64(x)
271
 
    return uniqs[s]
272
 
 
273
 
 
274
 
KILO = 1024
275
 
MEG = 1024 * KILO
276
 
GIG = 1024 * MEG
277
 
P95_MEG = int(0.9 * MEG)
278
 
P95_GIG = int(0.9 * GIG)
279
 
 
280
 
 
281
 
def human_size(size, min_divisor=0):
282
 
    size = int(size)
283
 
    if (size == 0) and (min_divisor == 0):
284
 
        return '0'
285
 
    if (size < 512) and (min_divisor == 0):
286
 
        return str(size)
287
 
 
288
 
    if (size >= P95_GIG) or (min_divisor >= GIG):
289
 
        divisor = GIG
290
 
    elif (size >= P95_MEG) or (min_divisor >= MEG):
291
 
        divisor = MEG
292
 
    else:
293
 
        divisor = KILO
294
 
 
295
 
    dot = size % divisor
296
 
    base = size - dot
297
 
    dot = dot * 10 // divisor
298
 
    base //= divisor
299
 
    if dot >= 10:
300
 
        base += 1
301
 
        dot -= 10
302
 
 
303
 
    out = str(base)
304
 
    if (base < 100) and (dot != 0):
305
 
        out += '.%d' % (dot)
306
 
    if divisor == KILO:
307
 
        out += 'K'
308
 
    elif divisor == MEG:
309
 
        out += 'M'
310
 
    elif divisor == GIG:
311
 
        out += 'G'
312
 
    return out
313
 
 
314
 
 
315
 
def fill_in_navigation(navigation):
316
 
    """
317
 
    given a navigation block (used by the template for the page header), fill
318
 
    in useful calculated values.
319
 
    """
320
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
321
 
        navigation.position = navigation.revid_list.index(navigation.revid)
322
 
    else:
323
 
        navigation.position = 0
324
 
    navigation.count = len(navigation.revid_list)
325
 
    navigation.page_position = navigation.position // navigation.pagesize + 1
326
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
327
 
 - 1)) // navigation.pagesize
328
 
 
329
 
    def get_offset(offset):
330
 
        if (navigation.position + offset < 0) or (
331
 
           navigation.position + offset > navigation.count - 1):
332
 
            return None
333
 
        return navigation.revid_list[navigation.position + offset]
334
 
 
335
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
336
 
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
337
 
    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}
345
 
    if getattr(navigation, 'query', None) is not None:
346
 
        params['q'] = navigation.query
347
 
 
348
 
    if getattr(navigation, 'start_revid', None) is not None:
349
 
        params['start_revid'] = start_revno
350
 
 
351
 
    if navigation.prev_page_revid:
352
 
        navigation.prev_page_url = navigation.branch.context_url(
353
 
            [navigation.scan_url, prev_page_revno], **params)
354
 
    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
422
 
 
423
 
 
424
 
def decorator(unbound):
425
 
 
426
 
    def new_decorator(f):
427
 
        g = unbound(f)
428
 
        g.__name__ = f.__name__
429
 
        g.__doc__ = f.__doc__
430
 
        g.__dict__.update(f.__dict__)
431
 
        return g
432
 
    new_decorator.__name__ = unbound.__name__
433
 
    new_decorator.__doc__ = unbound.__doc__
434
 
    new_decorator.__dict__.update(unbound.__dict__)
435
 
    return new_decorator
436
 
 
437
 
 
438
 
# common threading-lock decorator
439
 
 
440
 
 
441
 
def with_lock(lockname, debug_name=None):
442
 
    if debug_name is None:
443
 
        debug_name = lockname
444
 
 
445
 
    @decorator
446
 
    def _decorator(unbound):
447
 
 
448
 
        def locked(self, *args, **kw):
449
 
            getattr(self, lockname).acquire()
450
 
            try:
451
 
                return unbound(self, *args, **kw)
452
 
            finally:
453
 
                getattr(self, lockname).release()
454
 
        return locked
455
 
    return _decorator
456
 
 
457
 
 
458
 
@decorator
459
 
def lsprof(f):
460
 
 
461
 
    def _f(*a, **kw):
462
 
        from loggerhead.lsprof import profile
463
 
        import cPickle
464
 
        z = time.time()
465
 
        ret, stats = profile(f, *a, **kw)
466
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
467
 
            int((time.time() - z) * 1000)))
468
 
        stats.sort()
469
 
        stats.freeze()
470
 
        now = time.time()
471
 
        msec = int(now * 1000) % 1000
472
 
        timestr = time.strftime('%Y%m%d%H%M%S',
473
 
                                time.localtime(now)) + ('%03d' % msec)
474
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
475
 
        cPickle.dump(stats, open(filename, 'w'), 2)
476
 
        return ret
477
 
    return _f
478
 
 
479
 
 
480
 
# just thinking out loud here...
481
 
#
482
 
# so, when browsing around, there are 5 pieces of context, most optional:
483
 
#     - current revid
484
 
#         current location along the navigation path (while browsing)
485
 
#     - starting revid (start_revid)
486
 
#         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.
489
 
#     - file_id
490
 
#         the file being looked at
491
 
#     - filter_file_id
492
 
#         if navigating the revisions that touched a file
493
 
#     - q (query)
494
 
#         if navigating the revisions that matched a search query
495
 
#     - remember
496
 
#         a previous revision to remember for future comparisons
497
 
#
498
 
# current revid is given on the url path.  the rest are optional components
499
 
# in the url params.
500
 
#
501
 
# other transient things can be set:
502
 
#     - compare_revid
503
 
#         to compare one revision to another, on /revision only
504
 
#     - sort
505
 
#         for re-ordering an existing page by different sort
506
 
 
507
 
t_context = threading.local()
508
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
509
 
          'compare_revid', 'sort')
510
 
 
511
 
 
512
 
def set_context(map):
513
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
514
 
 
515
 
 
516
 
def get_context(**overrides):
517
 
    """
518
 
    Soon to be deprecated.
519
 
 
520
 
 
521
 
    return a context map that may be overriden by specific values passed in,
522
 
    but only contains keys from the list of valid context keys.
523
 
 
524
 
    if 'clear' is set, only the 'remember' context value will be added, and
525
 
    all other context will be omitted.
526
 
    """
527
 
    map = dict()
528
 
    if overrides.get('clear', False):
529
 
        map['remember'] = t_context.map.get('remember', None)
530
 
    else:
531
 
        map.update(t_context.map)
532
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
533
 
    map.update(overrides)
534
 
    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
 
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