~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:
17
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
18
#
19
19
 
20
 
from elementtree import ElementTree as ET
21
 
 
22
20
import base64
23
21
import cgi
24
22
import datetime
28
26
import struct
29
27
import sys
30
28
import threading
31
 
import time
32
29
import traceback
33
30
 
 
31
import turbogears
 
32
 
34
33
 
35
34
log = logging.getLogger("loggerhead.controllers")
36
35
 
37
 
# Display of times.
38
 
 
39
 
# date_day -- just the day
40
 
# date_time -- full date with time
41
 
#
42
 
# displaydate -- for use in sentences
43
 
# approximatedate -- for use in tables
44
 
#
45
 
# displaydate and approximatedate return an elementtree <span> Element
46
 
# with the full date in a tooltip.
47
 
 
48
 
def date_day(value):
49
 
    return value.strftime('%Y-%m-%d')
50
 
 
51
 
 
52
 
def date_time(value):
53
 
    return value.strftime('%Y-%m-%d %T')
54
 
 
55
 
 
56
 
def _displaydate(date):
57
 
    delta = abs(datetime.datetime.now() - date)
58
 
    if delta > datetime.timedelta(1, 0, 0):
59
 
        # far in the past or future, display the date
60
 
        return 'on ' + date_day(date)
61
 
    return _approximatedate(date)
62
 
 
63
 
 
64
 
def _approximatedate(date):
65
 
    delta = datetime.datetime.now() - date
66
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
67
 
        # far in the past or future, display the date
68
 
        return date_day(date)
69
 
    future = delta < datetime.timedelta(0, 0, 0)
70
 
    delta = abs(delta)
71
 
    days = delta.days
72
 
    hours = delta.seconds / 3600
73
 
    minutes = (delta.seconds - (3600*hours)) / 60
74
 
    seconds = delta.seconds % 60
75
 
    result = ''
76
 
    if future:
77
 
        result += 'in '
78
 
    if days != 0:
79
 
        amount = days
80
 
        unit = 'day'
81
 
    elif hours != 0:
82
 
        amount = hours
83
 
        unit = 'hour'
84
 
    elif minutes != 0:
85
 
        amount = minutes
86
 
        unit = 'minute'
87
 
    else:
88
 
        amount = seconds
89
 
        unit = 'second'
90
 
    if amount != 1:
91
 
        unit += 's'
92
 
    result += '%s %s' % (amount, unit)
93
 
    if not future:
94
 
        result += ' ago'
95
 
        return result
96
 
 
97
 
 
98
 
def _wrap_with_date_time_title(date, formatted_date):
99
 
    elem = ET.Element("span")
100
 
    elem.text = formatted_date
101
 
    elem.set("title", date_time(date))
102
 
    return elem
103
 
 
104
 
 
105
 
def approximatedate(date):
106
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
107
 
 
108
 
 
109
 
def displaydate(date):
110
 
    return _wrap_with_date_time_title(date, _displaydate(date))
 
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
 
 
72
def fix_year(year):
 
73
    if year < 70:
 
74
        year += 2000
 
75
    if year < 100:
 
76
        year += 1900
 
77
    return year
111
78
 
112
79
 
113
80
class Container (object):
120
87
                setattr(self, key, value)
121
88
        for key, value in kw.iteritems():
122
89
            setattr(self, key, value)
123
 
 
 
90
    
124
91
    def __repr__(self):
125
92
        out = '{ '
126
93
        for key, value in self.__dict__.iteritems():
175
142
        return '%s at %s' % (username, domains[-2])
176
143
    return '%s at %s' % (username, domains[0])
177
144
 
178
 
 
 
145
    
179
146
def triple_factors(min_value=1):
180
147
    factors = (1, 3)
181
148
    index = 0
193
160
    """
194
161
    given a position in a maximum range, return a list of negative and positive
195
162
    jump factors for an hgweb-style triple-factor geometric scan.
196
 
 
 
163
    
197
164
    for example, with pos=20 and max=500, the range would be:
198
165
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
199
 
 
 
166
    
200
167
    i admit this is a very strange way of jumping through revisions.  i didn't
201
168
    invent it. :)
202
169
    """
213
180
# only do this if unicode turns out to be a problem
214
181
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
215
182
 
216
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
217
183
def html_clean(s):
218
184
    """
219
185
    clean up a string for html display.  expand any tabs, encode any html
221
187
    in displaying monospace text.
222
188
    """
223
189
    s = cgi.escape(s.expandtabs())
 
190
#    s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
224
191
    s = s.replace(' ', '&nbsp;')
225
192
    return s
226
193
 
227
194
 
228
 
 
229
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
230
 
 
231
 
def fixed_width(s):
232
 
    """
233
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
234
 
    chop up the string.
235
 
    """
236
 
    if not isinstance(s, unicode):
237
 
        # this kinda sucks.  file contents are just binary data, and no
238
 
        # encoding metadata is stored, so we need to guess.  this is probably
239
 
        # okay for most code, but for people using things like KOI-8, this
240
 
        # will display gibberish.  we have no way of detecting the correct
241
 
        # encoding to use.
242
 
        try:
243
 
            s = s.decode('utf-8')
244
 
        except UnicodeDecodeError:
245
 
            s = s.decode('iso-8859-15')
246
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
247
 
 
248
 
 
249
195
def fake_permissions(kind, executable):
250
196
    # fake up unix-style permissions given only a "kind" and executable bit
251
197
    if kind == 'directory':
304
250
        divisor = MEG
305
251
    else:
306
252
        divisor = KILO
307
 
 
 
253
    
308
254
    dot = size % divisor
309
255
    base = size - dot
310
256
    dot = dot * 10 // divisor
312
258
    if dot >= 10:
313
259
        base += 1
314
260
        dot -= 10
315
 
 
 
261
    
316
262
    out = str(base)
317
263
    if (base < 100) and (dot != 0):
318
264
        out += '.%d' % (dot,)
323
269
    elif divisor == GIG:
324
270
        out += 'G'
325
271
    return out
326
 
 
327
 
 
328
 
def fill_in_navigation(navigation):
 
272
    
 
273
 
 
274
def fill_in_navigation(history, navigation):
329
275
    """
330
276
    given a navigation block (used by the template for the page header), fill
331
277
    in useful calculated values.
332
278
    """
333
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
334
 
        navigation.position = navigation.revid_list.index(navigation.revid)
335
 
    else:
 
279
    navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
 
280
    if navigation.position is None:
336
281
        navigation.position = 0
337
282
    navigation.count = len(navigation.revid_list)
338
283
    navigation.page_position = navigation.position // navigation.pagesize + 1
339
284
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
340
 
 
 
285
    
341
286
    def get_offset(offset):
342
287
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
343
288
            return None
344
289
        return navigation.revid_list[navigation.position + offset]
345
 
 
 
290
    
346
291
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
347
292
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
348
 
 
349
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
293
    
 
294
    params = { 'file_id': navigation.file_id }
350
295
    if getattr(navigation, 'query', None) is not None:
351
296
        params['q'] = navigation.query
352
297
    else:
353
298
        params['start_revid'] = navigation.start_revid
354
 
 
 
299
        
355
300
    if navigation.prev_page_revid:
356
 
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
 
301
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
357
302
    if navigation.next_page_revid:
358
 
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
 
303
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **params)
359
304
 
360
305
 
361
306
def log_exception(log):
383
328
    @decorator
384
329
    def _decorator(unbound):
385
330
        def locked(self, *args, **kw):
 
331
            #self.log.debug('-> %r lock %r', id(threading.currentThread()), debug_name)
386
332
            getattr(self, lockname).acquire()
387
333
            try:
388
334
                return unbound(self, *args, **kw)
389
335
            finally:
390
336
                getattr(self, lockname).release()
 
337
                #self.log.debug('<- %r unlock %r', id(threading.currentThread()), debug_name)
391
338
        return locked
392
339
    return _decorator
393
340
 
394
 
 
395
 
@decorator
396
 
def strip_whitespace(f):
397
 
    def _f(*a, **kw):
398
 
        out = f(*a, **kw)
399
 
        orig_len = len(out)
400
 
        out = re.sub(r'\n\s+', '\n', out)
401
 
        out = re.sub(r'[ \t]+', ' ', out)
402
 
        out = re.sub(r'\s+\n', '\n', out)
403
 
        new_len = len(out)
404
 
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
405
 
                  human_size(orig_len - new_len),
406
 
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
407
 
        return out
408
 
    return _f
409
 
 
410
 
 
411
 
@decorator
412
 
def lsprof(f):
413
 
    def _f(*a, **kw):
414
 
        from loggerhead.lsprof import profile
415
 
        import cPickle
416
 
        z = time.time()
417
 
        ret, stats = profile(f, *a, **kw)
418
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
419
 
        stats.sort()
420
 
        stats.freeze()
421
 
        now = time.time()
422
 
        msec = int(now * 1000) % 1000
423
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
424
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
425
 
        cPickle.dump(stats, open(filename, 'w'), 2)
426
 
        return ret
427
 
    return _f
428
 
 
429
 
 
430
 
# just thinking out loud here...
431
 
#
432
 
# so, when browsing around, there are 5 pieces of context, most optional:
433
 
#     - current revid
434
 
#         current location along the navigation path (while browsing)
435
 
#     - starting revid (start_revid)
436
 
#         the current beginning of navigation (navigation continues back to
437
 
#         the original revision) -- this defines an 'alternate mainline'
438
 
#         when the user navigates into a branch.
439
 
#     - file_id
440
 
#         the file being looked at
441
 
#     - filter_file_id
442
 
#         if navigating the revisions that touched a file
443
 
#     - q (query)
444
 
#         if navigating the revisions that matched a search query
445
 
#     - remember
446
 
#         a previous revision to remember for future comparisons
447
 
#
448
 
# current revid is given on the url path.  the rest are optional components
449
 
# in the url params.
450
 
#
451
 
# other transient things can be set:
452
 
#     - compare_revid
453
 
#         to compare one revision to another, on /revision only
454
 
#     - sort
455
 
#         for re-ordering an existing page by different sort
456
 
 
457
 
t_context = threading.local()
458
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
459
 
          'compare_revid', 'sort')
460
 
 
461
 
 
462
 
def set_context(map):
463
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
464
 
 
465
 
 
466
 
def get_context(**overrides):
467
 
    """
468
 
    return a context map that may be overriden by specific values passed in,
469
 
    but only contains keys from the list of valid context keys.
470
 
 
471
 
    if 'clear' is set, only the 'remember' context value will be added, and
472
 
    all other context will be omitted.
473
 
    """
474
 
    map = dict()
475
 
    if overrides.get('clear', False):
476
 
        map['remember'] = t_context.map.get('remember', None)
477
 
    else:
478
 
        map.update(t_context.map)
479
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
480
 
    map.update(overrides)
481
 
    return map