~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
 
def fix_year(year):
42
 
    if year < 70:
43
 
        year += 2000
44
 
    if year < 100:
45
 
        year += 1900
46
 
    return year
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))
 
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)
126
48
 
127
49
 
128
50
class Container (object):
136
58
        for key, value in kw.iteritems():
137
59
            setattr(self, key, value)
138
60
 
139
 
    def __repr__(self):
140
 
        out = '{ '
141
 
        for key, value in self.__dict__.iteritems():
142
 
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
143
 
                continue
144
 
            out += '%r => %r, ' % (key, value)
145
 
        out += '}'
146
 
        return out
147
 
 
148
 
 
149
 
def trunc(text, limit=10):
150
 
    if len(text) <= limit:
151
 
        return text
152
 
    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 ])
153
70
 
154
71
 
155
72
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
174
91
        return '%s at %s' % (username, domains[-2])
175
92
    return '%s at %s' % (username, domains[0])
176
93
 
177
 
 
178
 
# only do this if unicode turns out to be a problem
179
 
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
180
 
 
181
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
182
 
def html_clean(s):
183
 
    """
184
 
    clean up a string for html display.  expand any tabs, encode any html
185
 
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
186
 
    in displaying monospace text.
187
 
    """
188
 
    s = cgi.escape(s.expandtabs())
189
 
    s = s.replace(' ', '&nbsp;')
190
 
    return s
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
 
 
234
 
 
235
 
def fake_permissions(kind, executable):
236
 
    # fake up unix-style permissions given only a "kind" and executable bit
237
 
    if kind == 'directory':
238
 
        return 'drwxr-xr-x'
239
 
    if executable:
240
 
        return '-rwxr-xr-x'
241
 
    return '-rw-r--r--'
242
 
 
243
 
 
244
 
def b64(s):
245
 
    s = base64.encodestring(s).replace('\n', '')
246
 
    while (len(s) > 0) and (s[-1] == '='):
247
 
        s = s[:-1]
248
 
    return s
249
 
 
250
 
 
251
 
def uniq(uniqs, s):
252
 
    """
253
 
    turn a potentially long string into a unique smaller string.
254
 
    """
255
 
    if s in uniqs:
256
 
        return uniqs[s]
257
 
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
258
 
    x = struct.pack('>I', next)
259
 
    while (len(x) > 1) and (x[0] == '\x00'):
260
 
        x = x[1:]
261
 
    uniqs[s] = b64(x)
262
 
    return uniqs[s]
263
 
 
264
 
 
265
 
KILO = 1024
266
 
MEG = 1024 * KILO
267
 
GIG = 1024 * MEG
268
 
P95_MEG = int(0.9 * MEG)
269
 
P95_GIG = int(0.9 * GIG)
270
 
 
271
 
def human_size(size, min_divisor=0):
272
 
    size = int(size)
273
 
    if (size == 0) and (min_divisor == 0):
274
 
        return '0'
275
 
    if (size < 512) and (min_divisor == 0):
276
 
        return str(size)
277
 
 
278
 
    if (size >= P95_GIG) or (min_divisor >= GIG):
279
 
        divisor = GIG
280
 
    elif (size >= P95_MEG) or (min_divisor >= MEG):
281
 
        divisor = MEG
282
 
    else:
283
 
        divisor = KILO
284
 
 
285
 
    dot = size % divisor
286
 
    base = size - dot
287
 
    dot = dot * 10 // divisor
288
 
    base //= divisor
289
 
    if dot >= 10:
290
 
        base += 1
291
 
        dot -= 10
292
 
 
293
 
    out = str(base)
294
 
    if (base < 100) and (dot != 0):
295
 
        out += '.%d' % (dot,)
296
 
    if divisor == KILO:
297
 
        out += 'K'
298
 
    elif divisor == MEG:
299
 
        out += 'M'
300
 
    elif divisor == GIG:
301
 
        out += 'G'
302
 
    return out
303
 
 
304
 
 
305
 
def fill_in_navigation(navigation):
306
 
    """
307
 
    given a navigation block (used by the template for the page header), fill
308
 
    in useful calculated values.
309
 
    """
310
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
311
 
        navigation.position = navigation.revid_list.index(navigation.revid)
312
 
    else:
313
 
        navigation.position = 0
314
 
    navigation.count = len(navigation.revid_list)
315
 
    navigation.page_position = navigation.position // navigation.pagesize + 1
316
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
317
 
 
318
 
    def get_offset(offset):
319
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
320
 
            return None
321
 
        return navigation.revid_list[navigation.position + offset]
322
 
 
323
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
324
 
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
325
 
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
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 }
333
 
    if getattr(navigation, 'query', None) is not None:
334
 
        params['q'] = navigation.query
335
 
 
336
 
    if getattr(navigation, 'start_revid', None) is not None:
337
 
        params['start_revid'] = start_revno
338
 
 
339
 
    if navigation.prev_page_revid:
340
 
        navigation.prev_page_url = navigation.branch.context_url(
341
 
            [navigation.scan_url, prev_page_revno], **params)
342
 
    if navigation.next_page_revid:
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
410
 
 
411
 
 
412
 
def decorator(unbound):
413
 
    def new_decorator(f):
414
 
        g = unbound(f)
415
 
        g.__name__ = f.__name__
416
 
        g.__doc__ = f.__doc__
417
 
        g.__dict__.update(f.__dict__)
418
 
        return g
419
 
    new_decorator.__name__ = unbound.__name__
420
 
    new_decorator.__doc__ = unbound.__doc__
421
 
    new_decorator.__dict__.update(unbound.__dict__)
422
 
    return new_decorator
423
 
 
424
 
 
425
 
# common threading-lock decorator
426
 
def with_lock(lockname, debug_name=None):
427
 
    if debug_name is None:
428
 
        debug_name = lockname
429
 
    @decorator
430
 
    def _decorator(unbound):
431
 
        def locked(self, *args, **kw):
432
 
            getattr(self, lockname).acquire()
433
 
            try:
434
 
                return unbound(self, *args, **kw)
435
 
            finally:
436
 
                getattr(self, lockname).release()
437
 
        return locked
438
 
    return _decorator
439
 
 
440
 
 
441
 
@decorator
442
 
def lsprof(f):
443
 
    def _f(*a, **kw):
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
457
 
    return _f
458
 
 
459
 
 
460
 
# just thinking out loud here...
461
 
#
462
 
# so, when browsing around, there are 5 pieces of context, most optional:
463
 
#     - current revid
464
 
#         current location along the navigation path (while browsing)
465
 
#     - starting revid (start_revid)
466
 
#         the current beginning of navigation (navigation continues back to
467
 
#         the original revision) -- this defines an 'alternate mainline'
468
 
#         when the user navigates into a branch.
469
 
#     - file_id
470
 
#         the file being looked at
471
 
#     - filter_file_id
472
 
#         if navigating the revisions that touched a file
473
 
#     - q (query)
474
 
#         if navigating the revisions that matched a search query
475
 
#     - remember
476
 
#         a previous revision to remember for future comparisons
477
 
#
478
 
# current revid is given on the url path.  the rest are optional components
479
 
# in the url params.
480
 
#
481
 
# other transient things can be set:
482
 
#     - compare_revid
483
 
#         to compare one revision to another, on /revision only
484
 
#     - sort
485
 
#         for re-ordering an existing page by different sort
486
 
 
487
 
t_context = threading.local()
488
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
489
 
          'compare_revid', 'sort')
490
 
 
491
 
 
492
 
def set_context(map):
493
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
494
 
 
495
 
 
496
 
def get_context(**overrides):
497
 
    """
498
 
    Soon to be deprecated.
499
 
 
500
 
 
501
 
    return a context map that may be overriden by specific values passed in,
502
 
    but only contains keys from the list of valid context keys.
503
 
 
504
 
    if 'clear' is set, only the 'remember' context value will be added, and
505
 
    all other context will be omitted.
506
 
    """
507
 
    map = dict()
508
 
    if overrides.get('clear', False):
509
 
        map['remember'] = t_context.map.get('remember', None)
510
 
    else:
511
 
        map.update(t_context.map)
512
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
513
 
    map.update(overrides)
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
 
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