~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Michael Hudson
  • Date: 2008-02-27 03:13:45 UTC
  • mto: This revision was merged to the branch mainline in revision 145.
  • Revision ID: michael.hudson@canonical.com-20080227031345-p4uvpm3gj3806ajv
add just one docstring

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
 
 
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
 
41
73
def fix_year(year):
42
74
    if year < 70:
43
75
        year += 2000
45
77
        year += 1900
46
78
    return year
47
79
 
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):
 
80
 
 
81
_g_format = '%Y-%m-%d @ %H:%M'
 
82
 
 
83
def format_date(date):
 
84
    if _g_format == 'fancy':
 
85
        return fancy_format_date(date)
 
86
    return date.strftime(_g_format)
 
87
 
 
88
def fancy_format_date(date):
79
89
    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'
 
90
    if delta.days > 300:
 
91
        return date.strftime('%d %b %Y')
101
92
    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))
 
93
        return date.strftime('%d %b %H:%M')
 
94
 
 
95
def set_date_format(format):
 
96
    global _g_format
 
97
    _g_format = format
126
98
 
127
99
 
128
100
class Container (object):
135
107
                setattr(self, key, value)
136
108
        for key, value in kw.iteritems():
137
109
            setattr(self, key, value)
138
 
 
 
110
    
139
111
    def __repr__(self):
140
112
        out = '{ '
141
113
        for key, value in self.__dict__.iteritems():
146
118
        return out
147
119
 
148
120
 
 
121
def clean_revid(revid):
 
122
    if revid == 'missing':
 
123
        return revid
 
124
    return sha.new(revid).hexdigest()
 
125
 
 
126
 
 
127
def obfuscate(text):
 
128
    return ''.join([ '&#%d;' % ord(c) for c in text ])
 
129
 
 
130
 
149
131
def trunc(text, limit=10):
150
132
    if len(text) <= limit:
151
133
        return text
152
134
    return text[:limit] + '...'
153
135
 
154
136
 
 
137
def to_utf8(s):
 
138
    if isinstance(s, unicode):
 
139
        return s.encode('utf-8')
 
140
    return s
 
141
 
 
142
 
155
143
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
156
144
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
157
145
 
174
162
        return '%s at %s' % (username, domains[-2])
175
163
    return '%s at %s' % (username, domains[0])
176
164
 
 
165
    
 
166
def triple_factors(min_value=1):
 
167
    factors = (1, 3)
 
168
    index = 0
 
169
    n = 1
 
170
    while True:
 
171
        if n >= min_value:
 
172
            yield n * factors[index]
 
173
        index += 1
 
174
        if index >= len(factors):
 
175
            index = 0
 
176
            n *= 10
 
177
 
 
178
 
 
179
def scan_range(pos, max, pagesize=1):
 
180
    """
 
181
    given a position in a maximum range, return a list of negative and positive
 
182
    jump factors for an hgweb-style triple-factor geometric scan.
 
183
    
 
184
    for example, with pos=20 and max=500, the range would be:
 
185
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
186
    
 
187
    i admit this is a very strange way of jumping through revisions.  i didn't
 
188
    invent it. :)
 
189
    """
 
190
    out = []
 
191
    for n in triple_factors(pagesize + 1):
 
192
        if n > max:
 
193
            return out
 
194
        if pos + n < max:
 
195
            out.append(n)
 
196
        if pos - n >= 0:
 
197
            out.insert(0, -n)
 
198
 
177
199
 
178
200
# only do this if unicode turns out to be a problem
179
201
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
189
211
    s = s.replace(' ', '&nbsp;')
190
212
    return s
191
213
 
 
214
 
 
215
 
192
216
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
193
217
 
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
218
def fixed_width(s):
218
219
    """
219
220
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
241
242
    return '-rw-r--r--'
242
243
 
243
244
 
 
245
def if_present(format, value):
 
246
    """
 
247
    format a value using a format string, if the value exists and is not None.
 
248
    """
 
249
    if value is None:
 
250
        return ''
 
251
    return format % value
 
252
 
 
253
 
244
254
def b64(s):
245
255
    s = base64.encodestring(s).replace('\n', '')
246
256
    while (len(s) > 0) and (s[-1] == '='):
281
291
        divisor = MEG
282
292
    else:
283
293
        divisor = KILO
284
 
 
 
294
    
285
295
    dot = size % divisor
286
296
    base = size - dot
287
297
    dot = dot * 10 // divisor
289
299
    if dot >= 10:
290
300
        base += 1
291
301
        dot -= 10
292
 
 
 
302
    
293
303
    out = str(base)
294
304
    if (base < 100) and (dot != 0):
295
305
        out += '.%d' % (dot,)
300
310
    elif divisor == GIG:
301
311
        out += 'G'
302
312
    return out
303
 
 
 
313
    
304
314
 
305
315
def fill_in_navigation(navigation):
306
316
    """
314
324
    navigation.count = len(navigation.revid_list)
315
325
    navigation.page_position = navigation.position // navigation.pagesize + 1
316
326
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
317
 
 
 
327
    
318
328
    def get_offset(offset):
319
329
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
320
330
            return None
321
331
        return navigation.revid_list[navigation.position + offset]
322
 
 
323
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
 
332
    
324
333
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
325
334
    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 }
 
335
    
 
336
    params = { 'file_id': navigation.file_id }
333
337
    if getattr(navigation, 'query', None) is not None:
334
338
        params['q'] = navigation.query
335
 
 
336
 
    if getattr(navigation, 'start_revid', None) is not None:
337
 
        params['start_revid'] = start_revno
338
 
 
 
339
    else:
 
340
        params['start_revid'] = navigation.start_revid
 
341
        
339
342
    if navigation.prev_page_revid:
340
 
        navigation.prev_page_url = navigation.branch.context_url(
341
 
            [navigation.scan_url, prev_page_revno], **params)
 
343
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
342
344
    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
 
345
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
 
346
 
 
347
 
 
348
def log_exception(log):
 
349
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
350
        log.debug(line)
410
351
 
411
352
 
412
353
def decorator(unbound):
439
380
 
440
381
 
441
382
@decorator
 
383
def strip_whitespace(f):
 
384
    def _f(*a, **kw):
 
385
        out = f(*a, **kw)
 
386
        orig_len = len(out)
 
387
        out = re.sub(r'\n\s+', '\n', out)
 
388
        out = re.sub(r'[ \t]+', ' ', out)
 
389
        out = re.sub(r'\s+\n', '\n', out)
 
390
        new_len = len(out)
 
391
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
 
392
                  human_size(orig_len - new_len),
 
393
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
 
394
        return out
 
395
    return _f
 
396
 
 
397
 
 
398
@decorator
442
399
def lsprof(f):
443
400
    def _f(*a, **kw):
444
401
        from loggerhead.lsprof import profile
464
421
#         current location along the navigation path (while browsing)
465
422
#     - starting revid (start_revid)
466
423
#         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.
 
424
#         the original revision) -- this may not be along the primary revision
 
425
#         path since the user may have navigated into a branch
469
426
#     - file_id
470
 
#         the file being looked at
471
 
#     - filter_file_id
472
427
#         if navigating the revisions that touched a file
473
428
#     - q (query)
474
429
#         if navigating the revisions that matched a search query
485
440
#         for re-ordering an existing page by different sort
486
441
 
487
442
t_context = threading.local()
488
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
489
 
          'compare_revid', 'sort')
 
443
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
490
444
 
491
445
 
492
446
def set_context(map):
495
449
 
496
450
def get_context(**overrides):
497
451
    """
498
 
    Soon to be deprecated.
499
 
 
500
 
 
501
452
    return a context map that may be overriden by specific values passed in,
502
453
    but only contains keys from the list of valid context keys.
503
 
 
 
454
    
504
455
    if 'clear' is set, only the 'remember' context value will be added, and
505
456
    all other context will be omitted.
506
457
    """
512
463
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
513
464
    map.update(overrides)
514
465
    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