~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robey Pointer
  • Date: 2007-01-14 05:40:40 UTC
  • Revision ID: robey@lag.net-20070114054040-7i9lbhq992e612rq
fix up dev.cfg so that nobody will ever have to edit it, by letting the
important params be overridable in loggerhead.conf.

make start-loggerhead actually daemonize, write a pid file, and write logs
to normal log files, instead of requiring 'nohup' stuff.  ie act like a real
server.  added stop-loggerhead to do a clean shutdown.  changed the README
to clarify how it should work now.

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
 
import time
35
 
import types
 
29
import traceback
 
30
 
 
31
import turbogears
 
32
 
36
33
 
37
34
log = logging.getLogger("loggerhead.controllers")
38
35
 
 
36
 
 
37
def timespan(delta):
 
38
    if delta.days > 730:
 
39
        # good grief!
 
40
        return '%d years' % (int(delta.days // 365.25),)
 
41
    if delta.days >= 3:
 
42
        return '%d days' % delta.days
 
43
    seg = []
 
44
    if delta.days > 0:
 
45
        if delta.days == 1:
 
46
            seg.append('1 day')
 
47
        else:
 
48
            seg.append('%d days' % delta.days)
 
49
    hrs = delta.seconds // 3600
 
50
    mins = (delta.seconds % 3600) // 60
 
51
    if hrs > 0:
 
52
        if hrs == 1:
 
53
            seg.append('1 hour')
 
54
        else:
 
55
            seg.append('%d hours' % hrs)
 
56
    if delta.days == 0:
 
57
        if mins > 0:
 
58
            if mins == 1:
 
59
                seg.append('1 minute')
 
60
            else:
 
61
                seg.append('%d minutes' % mins)
 
62
        elif hrs == 0:
 
63
            seg.append('less than a minute')
 
64
    return ', '.join(seg)
 
65
 
 
66
 
 
67
def ago(timestamp):
 
68
    now = datetime.datetime.now()
 
69
    return timespan(now - timestamp) + ' ago'
 
70
 
 
71
 
39
72
def fix_year(year):
40
73
    if year < 70:
41
74
        year += 2000
43
76
        year += 1900
44
77
    return year
45
78
 
46
 
# Display of times.
47
 
 
48
 
# date_day -- just the day
49
 
# date_time -- full date with time
50
 
#
51
 
# displaydate -- for use in sentences
52
 
# approximatedate -- for use in tables
53
 
#
54
 
# displaydate and approximatedate return an elementtree <span> Element
55
 
# with the full date in a tooltip.
56
 
 
57
 
def date_day(value):
58
 
    return value.strftime('%Y-%m-%d')
59
 
 
60
 
 
61
 
def date_time(value):
62
 
    return value.strftime('%Y-%m-%d %T')
63
 
 
64
 
 
65
 
def _displaydate(date):
66
 
    delta = abs(datetime.datetime.now() - date)
67
 
    if delta > datetime.timedelta(1, 0, 0):
68
 
        # far in the past or future, display the date
69
 
        return 'on ' + date_day(date)
70
 
    return _approximatedate(date)
71
 
 
72
 
 
73
 
def _approximatedate(date):
74
 
    delta = datetime.datetime.now() - date
75
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
76
 
        # far in the past or future, display the date
77
 
        return date_day(date)
78
 
    future = delta < datetime.timedelta(0, 0, 0)
79
 
    delta = abs(delta)
80
 
    days = delta.days
81
 
    hours = delta.seconds / 3600
82
 
    minutes = (delta.seconds - (3600*hours)) / 60
83
 
    seconds = delta.seconds % 60
84
 
    result = ''
85
 
    if future:
86
 
        result += 'in '
87
 
    if days != 0:
88
 
        amount = days
89
 
        unit = 'day'
90
 
    elif hours != 0:
91
 
        amount = hours
92
 
        unit = 'hour'
93
 
    elif minutes != 0:
94
 
        amount = minutes
95
 
        unit = 'minute'
96
 
    else:
97
 
        amount = seconds
98
 
        unit = 'second'
99
 
    if amount != 1:
100
 
        unit += 's'
101
 
    result += '%s %s' % (amount, unit)
102
 
    if not future:
103
 
        result += ' ago'
104
 
        return result
105
 
 
106
 
 
107
 
def _wrap_with_date_time_title(date, formatted_date):
108
 
    elem = ET.Element("span")
109
 
    elem.text = formatted_date
110
 
    elem.set("title", date_time(date))
111
 
    return elem
112
 
 
113
 
 
114
 
def approximatedate(date):
115
 
    #FIXME: Returns an object instead of a string
116
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
117
 
 
118
 
 
119
 
def displaydate(date):
120
 
    return _wrap_with_date_time_title(date, _displaydate(date))
121
 
 
122
79
 
123
80
class Container (object):
124
81
    """
130
87
                setattr(self, key, value)
131
88
        for key, value in kw.iteritems():
132
89
            setattr(self, key, value)
133
 
 
 
90
    
134
91
    def __repr__(self):
135
92
        out = '{ '
136
93
        for key, value in self.__dict__.iteritems():
141
98
        return out
142
99
 
143
100
 
 
101
def clean_revid(revid):
 
102
    if revid == 'missing':
 
103
        return revid
 
104
    return sha.new(revid).hexdigest()
 
105
 
 
106
 
 
107
def obfuscate(text):
 
108
    return ''.join([ '&#%d;' % ord(c) for c in text ])
 
109
 
 
110
 
144
111
def trunc(text, limit=10):
145
112
    if len(text) <= limit:
146
113
        return text
147
114
    return text[:limit] + '...'
148
115
 
149
116
 
 
117
def to_utf8(s):
 
118
    if isinstance(s, unicode):
 
119
        return s.encode('utf-8')
 
120
    return s
 
121
 
 
122
 
150
123
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
151
124
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
152
125
 
169
142
        return '%s at %s' % (username, domains[-2])
170
143
    return '%s at %s' % (username, domains[0])
171
144
 
 
145
    
 
146
def triple_factors(min_value=1):
 
147
    factors = (1, 3)
 
148
    index = 0
 
149
    n = 1
 
150
    while True:
 
151
        if n >= min_value:
 
152
            yield n * factors[index]
 
153
        index += 1
 
154
        if index >= len(factors):
 
155
            index = 0
 
156
            n *= 10
 
157
 
 
158
 
 
159
def scan_range(pos, max, pagesize=1):
 
160
    """
 
161
    given a position in a maximum range, return a list of negative and positive
 
162
    jump factors for an hgweb-style triple-factor geometric scan.
 
163
    
 
164
    for example, with pos=20 and max=500, the range would be:
 
165
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
166
    
 
167
    i admit this is a very strange way of jumping through revisions.  i didn't
 
168
    invent it. :)
 
169
    """
 
170
    out = []
 
171
    for n in triple_factors(pagesize + 1):
 
172
        if n > max:
 
173
            return out
 
174
        if pos + n < max:
 
175
            out.append(n)
 
176
        if pos - n >= 0:
 
177
            out.insert(0, -n)
 
178
 
172
179
 
173
180
# only do this if unicode turns out to be a problem
174
181
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
175
182
 
176
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
177
183
def html_clean(s):
178
184
    """
179
185
    clean up a string for html display.  expand any tabs, encode any html
181
187
    in displaying monospace text.
182
188
    """
183
189
    s = cgi.escape(s.expandtabs())
 
190
#    s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
184
191
    s = s.replace(' ', '&nbsp;')
185
192
    return s
186
193
 
187
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
188
 
 
189
 
def fill_div(s):
190
 
    """
191
 
    CSS is stupid. In some cases we need to replace an empty value with
192
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
193
 
 
194
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
195
 
    """
196
 
    
197
 
 
198
 
    if s is None:
199
 
        return '&nbsp;'
200
 
    elif isinstance(s, int):
201
 
        return s
202
 
    elif not s.strip():
203
 
        return '&nbsp;'
204
 
    else:
205
 
        try:
206
 
            s = s.decode('utf-8')
207
 
        except UnicodeDecodeError:
208
 
            s = s.decode('iso-8859-15')
209
 
        return s
210
 
 
211
 
 
212
 
def fixed_width(s):
213
 
    """
214
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
215
 
    chop up the string.
216
 
    """
217
 
    if not isinstance(s, unicode):
218
 
        # this kinda sucks.  file contents are just binary data, and no
219
 
        # encoding metadata is stored, so we need to guess.  this is probably
220
 
        # okay for most code, but for people using things like KOI-8, this
221
 
        # will display gibberish.  we have no way of detecting the correct
222
 
        # encoding to use.
223
 
        try:
224
 
            s = s.decode('utf-8')
225
 
        except UnicodeDecodeError:
226
 
            s = s.decode('iso-8859-15')
227
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
228
 
 
229
194
 
230
195
def fake_permissions(kind, executable):
231
196
    # fake up unix-style permissions given only a "kind" and executable bit
236
201
    return '-rw-r--r--'
237
202
 
238
203
 
 
204
def if_present(format, value):
 
205
    """
 
206
    format a value using a format string, if the value exists and is not None.
 
207
    """
 
208
    if value is None:
 
209
        return ''
 
210
    return format % value
 
211
 
 
212
 
239
213
def b64(s):
240
214
    s = base64.encodestring(s).replace('\n', '')
241
215
    while (len(s) > 0) and (s[-1] == '='):
276
250
        divisor = MEG
277
251
    else:
278
252
        divisor = KILO
279
 
 
 
253
    
280
254
    dot = size % divisor
281
255
    base = size - dot
282
256
    dot = dot * 10 // divisor
284
258
    if dot >= 10:
285
259
        base += 1
286
260
        dot -= 10
287
 
 
 
261
    
288
262
    out = str(base)
289
263
    if (base < 100) and (dot != 0):
290
264
        out += '.%d' % (dot,)
295
269
    elif divisor == GIG:
296
270
        out += 'G'
297
271
    return out
298
 
 
299
 
 
300
 
def fill_in_navigation(navigation):
 
272
    
 
273
 
 
274
def fill_in_navigation(history, navigation):
301
275
    """
302
276
    given a navigation block (used by the template for the page header), fill
303
277
    in useful calculated values.
304
278
    """
305
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
306
 
        navigation.position = navigation.revid_list.index(navigation.revid)
307
 
    else:
 
279
    navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
 
280
    if navigation.position is None:
308
281
        navigation.position = 0
309
282
    navigation.count = len(navigation.revid_list)
310
283
    navigation.page_position = navigation.position // navigation.pagesize + 1
311
284
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
312
 
 
 
285
    
313
286
    def get_offset(offset):
314
287
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
315
288
            return None
316
289
        return navigation.revid_list[navigation.position + offset]
317
 
 
318
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
 
290
    
319
291
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
320
292
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
321
 
    prev_page_revno = navigation.history.get_revno(
322
 
            navigation.prev_page_revid)
323
 
    next_page_revno = navigation.history.get_revno(
324
 
            navigation.next_page_revid)
325
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
326
 
 
327
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
293
    
 
294
    params = { 'file_id': navigation.file_id }
328
295
    if getattr(navigation, 'query', None) is not None:
329
296
        params['q'] = navigation.query
330
 
 
331
 
    if getattr(navigation, 'start_revid', None) is not None:
332
 
        params['start_revid'] = start_revno
333
 
 
 
297
    else:
 
298
        params['start_revid'] = navigation.start_revid
 
299
        
334
300
    if navigation.prev_page_revid:
335
 
        navigation.prev_page_url = navigation.branch.context_url(
336
 
            [navigation.scan_url, prev_page_revno], **params)
 
301
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
337
302
    if navigation.next_page_revid:
338
 
        navigation.next_page_url = navigation.branch.context_url(
339
 
            [navigation.scan_url, next_page_revno], **params)
 
303
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **params)
 
304
 
 
305
 
 
306
def log_exception(log):
 
307
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
308
        log.debug(line)
340
309
 
341
310
 
342
311
def decorator(unbound):
359
328
    @decorator
360
329
    def _decorator(unbound):
361
330
        def locked(self, *args, **kw):
 
331
            #self.log.debug('-> %r lock %r', id(threading.currentThread()), debug_name)
362
332
            getattr(self, lockname).acquire()
363
333
            try:
364
334
                return unbound(self, *args, **kw)
365
335
            finally:
366
336
                getattr(self, lockname).release()
 
337
                #self.log.debug('<- %r unlock %r', id(threading.currentThread()), debug_name)
367
338
        return locked
368
339
    return _decorator
369
340
 
370
 
 
371
 
@decorator
372
 
def lsprof(f):
373
 
    def _f(*a, **kw):
374
 
        from loggerhead.lsprof import profile
375
 
        import cPickle
376
 
        z = time.time()
377
 
        ret, stats = profile(f, *a, **kw)
378
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
379
 
        stats.sort()
380
 
        stats.freeze()
381
 
        now = time.time()
382
 
        msec = int(now * 1000) % 1000
383
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
384
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
385
 
        cPickle.dump(stats, open(filename, 'w'), 2)
386
 
        return ret
387
 
    return _f
388
 
 
389
 
 
390
 
# just thinking out loud here...
391
 
#
392
 
# so, when browsing around, there are 5 pieces of context, most optional:
393
 
#     - current revid
394
 
#         current location along the navigation path (while browsing)
395
 
#     - starting revid (start_revid)
396
 
#         the current beginning of navigation (navigation continues back to
397
 
#         the original revision) -- this defines an 'alternate mainline'
398
 
#         when the user navigates into a branch.
399
 
#     - file_id
400
 
#         the file being looked at
401
 
#     - filter_file_id
402
 
#         if navigating the revisions that touched a file
403
 
#     - q (query)
404
 
#         if navigating the revisions that matched a search query
405
 
#     - remember
406
 
#         a previous revision to remember for future comparisons
407
 
#
408
 
# current revid is given on the url path.  the rest are optional components
409
 
# in the url params.
410
 
#
411
 
# other transient things can be set:
412
 
#     - compare_revid
413
 
#         to compare one revision to another, on /revision only
414
 
#     - sort
415
 
#         for re-ordering an existing page by different sort
416
 
 
417
 
t_context = threading.local()
418
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
419
 
          'compare_revid', 'sort')
420
 
 
421
 
 
422
 
def set_context(map):
423
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
424
 
 
425
 
 
426
 
def get_context(**overrides):
427
 
    """
428
 
    Soon to be deprecated.
429
 
 
430
 
 
431
 
    return a context map that may be overriden by specific values passed in,
432
 
    but only contains keys from the list of valid context keys.
433
 
 
434
 
    if 'clear' is set, only the 'remember' context value will be added, and
435
 
    all other context will be omitted.
436
 
    """
437
 
    map = dict()
438
 
    if overrides.get('clear', False):
439
 
        map['remember'] = t_context.map.get('remember', None)
440
 
    else:
441
 
        map.update(t_context.map)
442
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
443
 
    map.update(overrides)
444
 
    return map