~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
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
3
 
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
4
3
#
5
4
# This program is free software; you can redistribute it and/or modify
6
5
# it under the terms of the GNU General Public License as published by
17
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
17
#
19
18
 
20
 
import base64
21
 
import cgi
22
 
import datetime
23
 
import logging
24
19
import re
25
20
import sha
26
 
import struct
27
 
import sys
28
 
import threading
29
 
import time
30
 
import traceback
31
 
 
32
 
 
33
 
log = logging.getLogger("loggerhead.controllers")
34
 
 
35
 
def day_date(value):
36
 
    return value.strftime('%Y-%m-%d')
37
 
 
38
 
def displaydate(date):
39
 
    delta = abs(datetime.datetime.now() - date)
40
 
    if delta > datetime.timedelta(1, 0, 0):
41
 
        # far in the past or future, display the date
42
 
        return 'on ' + day_date(date)
43
 
    return approximatedate(date)
44
 
 
45
 
def approximatedate(date):
46
 
    delta = datetime.datetime.now() - date
47
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
48
 
        # far in the past or future, display the date
49
 
        return day_date(date)
50
 
    future = delta < datetime.timedelta(0, 0, 0)
51
 
    delta = abs(delta)
52
 
    days = delta.days
53
 
    hours = delta.seconds / 3600
54
 
    minutes = (delta.seconds - (3600*hours)) / 60
55
 
    seconds = delta.seconds % 60
56
 
    result = ''
57
 
    if future:
58
 
        result += 'in '
59
 
    if days != 0:
60
 
        amount = days
61
 
        unit = 'day'
62
 
    elif hours != 0:
63
 
        amount = hours
64
 
        unit = 'hour'
65
 
    elif minutes != 0:
66
 
        amount = minutes
67
 
        unit = 'minute'
68
 
    else:
69
 
        amount = seconds
70
 
        unit = 'second'
71
 
    if amount != 1:
72
 
        unit += 's'
73
 
    result += '%s %s' % (amount, unit)
74
 
    if not future:
75
 
        result += ' ago'
76
 
        return result
77
 
 
78
 
def lp_format_date(date):
79
 
    from elementtree import ElementTree as ET
80
 
    elem = ET.Element("span")
81
 
    elem.text = approximatedate(date)
82
 
    elem.set("title", date.strftime("%Y-%m-%d %T"))
83
 
    return elem
 
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)
84
48
 
85
49
 
86
50
class Container (object):
94
58
        for key, value in kw.iteritems():
95
59
            setattr(self, key, value)
96
60
 
97
 
    def __repr__(self):
98
 
        out = '{ '
99
 
        for key, value in self.__dict__.iteritems():
100
 
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
101
 
                continue
102
 
            out += '%r => %r, ' % (key, value)
103
 
        out += '}'
104
 
        return out
105
 
 
106
61
 
107
62
def clean_revid(revid):
108
63
    if revid == 'missing':
114
69
    return ''.join([ '&#%d;' % ord(c) for c in text ])
115
70
 
116
71
 
117
 
def trunc(text, limit=10):
118
 
    if len(text) <= limit:
119
 
        return text
120
 
    return text[:limit] + '...'
121
 
 
122
 
 
123
 
def to_utf8(s):
124
 
    if isinstance(s, unicode):
125
 
        return s.encode('utf-8')
126
 
    return s
127
 
 
128
 
 
129
72
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
130
73
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
131
74
 
148
91
        return '%s at %s' % (username, domains[-2])
149
92
    return '%s at %s' % (username, domains[0])
150
93
 
151
 
 
152
 
def triple_factors(min_value=1):
 
94
    
 
95
def triple_factors():
153
96
    factors = (1, 3)
154
97
    index = 0
155
98
    n = 1
156
99
    while True:
157
 
        if n >= min_value:
158
 
            yield n * factors[index]
 
100
        yield n * factors[index]
159
101
        index += 1
160
102
        if index >= len(factors):
161
103
            index = 0
162
104
            n *= 10
163
105
 
164
106
 
165
 
def scan_range(pos, max, pagesize=1):
 
107
def scan_range(pos, max):
166
108
    """
167
109
    given a position in a maximum range, return a list of negative and positive
168
110
    jump factors for an hgweb-style triple-factor geometric scan.
169
 
 
 
111
    
170
112
    for example, with pos=20 and max=500, the range would be:
171
113
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
172
 
 
 
114
    
173
115
    i admit this is a very strange way of jumping through revisions.  i didn't
174
116
    invent it. :)
175
117
    """
176
118
    out = []
177
 
    for n in triple_factors(pagesize + 1):
 
119
    for n in triple_factors():
178
120
        if n > max:
179
121
            return out
180
122
        if pos + n < max:
182
124
        if pos - n >= 0:
183
125
            out.insert(0, -n)
184
126
 
185
 
 
186
 
# only do this if unicode turns out to be a problem
187
 
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
188
 
 
189
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
190
 
def html_clean(s):
191
 
    """
192
 
    clean up a string for html display.  expand any tabs, encode any html
193
 
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
194
 
    in displaying monospace text.
195
 
    """
196
 
    s = cgi.escape(s.expandtabs())
197
 
    s = s.replace(' ', '&nbsp;')
198
 
    return s
199
 
 
200
 
 
201
 
 
202
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
203
 
 
204
 
def fixed_width(s):
205
 
    """
206
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
207
 
    chop up the string.
208
 
    """
209
 
    if not isinstance(s, unicode):
210
 
        # this kinda sucks.  file contents are just binary data, and no
211
 
        # encoding metadata is stored, so we need to guess.  this is probably
212
 
        # okay for most code, but for people using things like KOI-8, this
213
 
        # will display gibberish.  we have no way of detecting the correct
214
 
        # encoding to use.
215
 
        try:
216
 
            s = s.decode('utf-8')
217
 
        except UnicodeDecodeError:
218
 
            s = s.decode('iso-8859-15')
219
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
220
 
 
221
 
 
222
 
def fake_permissions(kind, executable):
223
 
    # fake up unix-style permissions given only a "kind" and executable bit
224
 
    if kind == 'directory':
225
 
        return 'drwxr-xr-x'
226
 
    if executable:
227
 
        return '-rwxr-xr-x'
228
 
    return '-rw-r--r--'
229
 
 
230
 
 
231
 
def if_present(format, value):
232
 
    """
233
 
    format a value using a format string, if the value exists and is not None.
234
 
    """
235
 
    if value is None:
236
 
        return ''
237
 
    return format % value
238
 
 
239
 
 
240
 
def b64(s):
241
 
    s = base64.encodestring(s).replace('\n', '')
242
 
    while (len(s) > 0) and (s[-1] == '='):
243
 
        s = s[:-1]
244
 
    return s
245
 
 
246
 
 
247
 
def uniq(uniqs, s):
248
 
    """
249
 
    turn a potentially long string into a unique smaller string.
250
 
    """
251
 
    if s in uniqs:
252
 
        return uniqs[s]
253
 
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
254
 
    x = struct.pack('>I', next)
255
 
    while (len(x) > 1) and (x[0] == '\x00'):
256
 
        x = x[1:]
257
 
    uniqs[s] = b64(x)
258
 
    return uniqs[s]
259
 
 
260
 
 
261
 
KILO = 1024
262
 
MEG = 1024 * KILO
263
 
GIG = 1024 * MEG
264
 
P95_MEG = int(0.9 * MEG)
265
 
P95_GIG = int(0.9 * GIG)
266
 
 
267
 
def human_size(size, min_divisor=0):
268
 
    size = int(size)
269
 
    if (size == 0) and (min_divisor == 0):
270
 
        return '0'
271
 
    if (size < 512) and (min_divisor == 0):
272
 
        return str(size)
273
 
 
274
 
    if (size >= P95_GIG) or (min_divisor >= GIG):
275
 
        divisor = GIG
276
 
    elif (size >= P95_MEG) or (min_divisor >= MEG):
277
 
        divisor = MEG
278
 
    else:
279
 
        divisor = KILO
280
 
 
281
 
    dot = size % divisor
282
 
    base = size - dot
283
 
    dot = dot * 10 // divisor
284
 
    base //= divisor
285
 
    if dot >= 10:
286
 
        base += 1
287
 
        dot -= 10
288
 
 
289
 
    out = str(base)
290
 
    if (base < 100) and (dot != 0):
291
 
        out += '.%d' % (dot,)
292
 
    if divisor == KILO:
293
 
        out += 'K'
294
 
    elif divisor == MEG:
295
 
        out += 'M'
296
 
    elif divisor == GIG:
297
 
        out += 'G'
298
 
    return out
299
 
 
300
 
 
301
 
def fill_in_navigation(navigation):
302
 
    """
303
 
    given a navigation block (used by the template for the page header), fill
304
 
    in useful calculated values.
305
 
    """
306
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
307
 
        navigation.position = navigation.revid_list.index(navigation.revid)
308
 
    else:
309
 
        navigation.position = 0
310
 
    navigation.count = len(navigation.revid_list)
311
 
    navigation.page_position = navigation.position // navigation.pagesize + 1
312
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
313
 
 
314
 
    def get_offset(offset):
315
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
316
 
            return None
317
 
        return navigation.revid_list[navigation.position + offset]
318
 
 
319
 
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
320
 
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
321
 
 
322
 
    params = { 'file_id': navigation.file_id }
323
 
    if getattr(navigation, 'query', None) is not None:
324
 
        params['q'] = navigation.query
325
 
    else:
326
 
        params['start_revid'] = navigation.start_revid
327
 
 
328
 
    if navigation.prev_page_revid:
329
 
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
330
 
    if navigation.next_page_revid:
331
 
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
332
 
 
333
 
 
334
 
def log_exception(log):
335
 
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
336
 
        log.debug(line)
337
 
 
338
 
 
339
 
def decorator(unbound):
340
 
    def new_decorator(f):
341
 
        g = unbound(f)
342
 
        g.__name__ = f.__name__
343
 
        g.__doc__ = f.__doc__
344
 
        g.__dict__.update(f.__dict__)
345
 
        return g
346
 
    new_decorator.__name__ = unbound.__name__
347
 
    new_decorator.__doc__ = unbound.__doc__
348
 
    new_decorator.__dict__.update(unbound.__dict__)
349
 
    return new_decorator
350
 
 
351
 
 
352
 
# common threading-lock decorator
353
 
def with_lock(lockname, debug_name=None):
354
 
    if debug_name is None:
355
 
        debug_name = lockname
356
 
    @decorator
357
 
    def _decorator(unbound):
358
 
        def locked(self, *args, **kw):
359
 
            getattr(self, lockname).acquire()
360
 
            try:
361
 
                return unbound(self, *args, **kw)
362
 
            finally:
363
 
                getattr(self, lockname).release()
364
 
        return locked
365
 
    return _decorator
366
 
 
367
 
 
368
 
@decorator
369
 
def strip_whitespace(f):
370
 
    def _f(*a, **kw):
371
 
        out = f(*a, **kw)
372
 
        orig_len = len(out)
373
 
        out = re.sub(r'\n\s+', '\n', out)
374
 
        out = re.sub(r'[ \t]+', ' ', out)
375
 
        out = re.sub(r'\s+\n', '\n', out)
376
 
        new_len = len(out)
377
 
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
378
 
                  human_size(orig_len - new_len),
379
 
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
380
 
        return out
381
 
    return _f
382
 
 
383
 
 
384
 
@decorator
385
 
def lsprof(f):
386
 
    def _f(*a, **kw):
387
 
        from loggerhead.lsprof import profile
388
 
        import cPickle
389
 
        z = time.time()
390
 
        ret, stats = profile(f, *a, **kw)
391
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
392
 
        stats.sort()
393
 
        stats.freeze()
394
 
        now = time.time()
395
 
        msec = int(now * 1000) % 1000
396
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
397
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
398
 
        cPickle.dump(stats, open(filename, 'w'), 2)
399
 
        return ret
400
 
    return _f
401
 
 
402
 
 
403
 
# just thinking out loud here...
404
 
#
405
 
# so, when browsing around, there are 5 pieces of context, most optional:
406
 
#     - current revid
407
 
#         current location along the navigation path (while browsing)
408
 
#     - starting revid (start_revid)
409
 
#         the current beginning of navigation (navigation continues back to
410
 
#         the original revision) -- this may not be along the primary revision
411
 
#         path since the user may have navigated into a branch
412
 
#     - file_id
413
 
#         if navigating the revisions that touched a file
414
 
#     - q (query)
415
 
#         if navigating the revisions that matched a search query
416
 
#     - remember
417
 
#         a previous revision to remember for future comparisons
418
 
#
419
 
# current revid is given on the url path.  the rest are optional components
420
 
# in the url params.
421
 
#
422
 
# other transient things can be set:
423
 
#     - compare_revid
424
 
#         to compare one revision to another, on /revision only
425
 
#     - sort
426
 
#         for re-ordering an existing page by different sort
427
 
 
428
 
t_context = threading.local()
429
 
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
430
 
 
431
 
 
432
 
def set_context(map):
433
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
434
 
 
435
 
 
436
 
def get_context(**overrides):
437
 
    """
438
 
    return a context map that may be overriden by specific values passed in,
439
 
    but only contains keys from the list of valid context keys.
440
 
 
441
 
    if 'clear' is set, only the 'remember' context value will be added, and
442
 
    all other context will be omitted.
443
 
    """
444
 
    map = dict()
445
 
    if overrides.get('clear', False):
446
 
        map['remember'] = t_context.map.get('remember', None)
447
 
    else:
448
 
        map.update(t_context.map)
449
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
450
 
    map.update(overrides)
451
 
    return map