~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robert Collins
  • Date: 2012-02-02 07:42:24 UTC
  • Revision ID: robertc@robertcollins.net-20120202074224-ujea2ocm1u1ws1en
    - Make tz calculations consistent and use UTC in the UI everywhere we show
      a precise timestamp. (Robert Collins, #594591)

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
20
#
21
21
 
22
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
22
import base64
28
 
import cgi
29
23
import datetime
30
24
import logging
31
25
import re
32
26
import struct
33
27
import threading
34
28
import time
35
 
import types
36
29
import sys
37
30
import os
38
31
import subprocess
39
32
 
 
33
try:
 
34
    from xml.etree import ElementTree as ET
 
35
except ImportError:
 
36
    from elementtree import ElementTree as ET
 
37
 
 
38
from bzrlib import urlutils
 
39
 
 
40
from simpletal.simpleTALUtils import HTMLStructureCleaner
 
41
 
40
42
log = logging.getLogger("loggerhead.controllers")
41
43
 
 
44
 
42
45
def fix_year(year):
43
46
    if year < 70:
44
47
        year += 2000
49
52
# Display of times.
50
53
 
51
54
# date_day -- just the day
52
 
# date_time -- full date with time
 
55
# date_time -- full date with time (UTC)
53
56
#
54
 
# displaydate -- for use in sentences
55
57
# approximatedate -- for use in tables
56
58
#
57
 
# displaydate and approximatedate return an elementtree <span> Element
58
 
# with the full date in a tooltip.
 
59
# approximatedate return an elementtree <span> Element
 
60
# with the full date (UTC) in a tooltip.
 
61
 
59
62
 
60
63
def date_day(value):
61
64
    return value.strftime('%Y-%m-%d')
63
66
 
64
67
def date_time(value):
65
68
    if value is not None:
66
 
        return value.strftime('%Y-%m-%d %T')
 
69
        # Note: this assumes that the value is UTC in some fashion.
 
70
        return value.strftime('%Y-%m-%d %H:%M:%S UTC')
67
71
    else:
68
72
        return 'N/A'
69
73
 
70
74
 
71
 
def _displaydate(date):
72
 
    delta = abs(datetime.datetime.now() - date)
73
 
    if delta > datetime.timedelta(1, 0, 0):
74
 
        # far in the past or future, display the date
75
 
        return 'on ' + date_day(date)
76
 
    return _approximatedate(date)
77
 
 
78
 
 
79
75
def _approximatedate(date):
80
76
    delta = datetime.datetime.now() - date
81
77
    if abs(delta) > datetime.timedelta(1, 0, 0):
122
118
    return _wrap_with_date_time_title(date, _approximatedate(date))
123
119
 
124
120
 
125
 
def displaydate(date):
126
 
    return _wrap_with_date_time_title(date, _displaydate(date))
127
 
 
128
 
 
129
 
class Container (object):
 
121
class Container(object):
130
122
    """
131
123
    Convert a dict into an object with attributes.
132
124
    """
 
125
 
133
126
    def __init__(self, _dict=None, **kw):
 
127
        self._properties = {}
134
128
        if _dict is not None:
135
129
            for key, value in _dict.iteritems():
136
130
                setattr(self, key, value)
140
134
    def __repr__(self):
141
135
        out = '{ '
142
136
        for key, value in self.__dict__.iteritems():
143
 
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
 
137
            if key.startswith('_') or (getattr(self.__dict__[key],
 
138
                                       '__call__', None) is not None):
144
139
                continue
145
140
            out += '%r => %r, ' % (key, value)
146
141
        out += '}'
147
142
        return out
148
143
 
 
144
    def __getattr__(self, attr):
 
145
        """Used for handling things that aren't already available."""
 
146
        if attr.startswith('_') or attr not in self._properties:
 
147
            raise AttributeError('No attribute: %s' % (attr,))
 
148
        val = self._properties[attr](self, attr)
 
149
        setattr(self, attr, val)
 
150
        return val
 
151
 
 
152
    def _set_property(self, attr, prop_func):
 
153
        """Set a function that will be called when an attribute is desired.
 
154
 
 
155
        We will cache the return value, so the function call should be
 
156
        idempotent. We will pass 'self' and the 'attr' name when triggered.
 
157
        """
 
158
        if attr.startswith('_'):
 
159
            raise ValueError("Cannot create properties that start with _")
 
160
        self._properties[attr] = prop_func
 
161
 
149
162
 
150
163
def trunc(text, limit=10):
151
164
    if len(text) <= limit:
156
169
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
157
170
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
158
171
 
 
172
 
159
173
def hide_email(email):
160
174
    """
161
175
    try to obsure any email address in a bazaar committer's name.
175
189
        return '%s at %s' % (username, domains[-2])
176
190
    return '%s at %s' % (username, domains[0])
177
191
 
 
192
def hide_emails(emails):
 
193
    """
 
194
    try to obscure any email address in a list of bazaar committers' names.
 
195
    """
 
196
    result = []
 
197
    for email in emails:
 
198
        result.append(hide_email(email))
 
199
    return result
178
200
 
179
201
# only do this if unicode turns out to be a problem
180
202
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
181
203
 
 
204
# Can't be a dict; &amp; needs to be done first.
 
205
html_entity_subs = [
 
206
    ("&", "&amp;"),
 
207
    ('"', "&quot;"),
 
208
    ("'", "&#39;"), # &apos; is defined in XML, but not HTML.
 
209
    (">", "&gt;"),
 
210
    ("<", "&lt;"),
 
211
    ]
 
212
 
 
213
 
 
214
def html_escape(s):
 
215
    """Transform dangerous (X)HTML characters into entities.
 
216
 
 
217
    Like cgi.escape, except also escaping \" and '. This makes it safe to use
 
218
    in both attribute and element content.
 
219
 
 
220
    If you want to safely fill a format string with escaped values, use
 
221
    html_format instead
 
222
    """
 
223
    for char, repl in html_entity_subs:
 
224
        s = s.replace(char, repl)
 
225
    return s
 
226
 
 
227
 
 
228
def html_format(template, *args):
 
229
    """Safely format an HTML template string, escaping the arguments.
 
230
 
 
231
    The template string must not be user-controlled; it will not be escaped.
 
232
    """
 
233
    return template % tuple(html_escape(arg) for arg in args)
 
234
 
 
235
 
182
236
# FIXME: get rid of this method; use fixed_width() and avoid XML().
 
237
 
183
238
def html_clean(s):
184
239
    """
185
240
    clean up a string for html display.  expand any tabs, encode any html
186
241
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
187
242
    in displaying monospace text.
188
243
    """
189
 
    s = cgi.escape(s.expandtabs())
 
244
    s = html_escape(s.expandtabs())
190
245
    s = s.replace(' ', '&nbsp;')
191
246
    return s
192
247
 
 
248
 
193
249
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
194
250
 
 
251
 
195
252
def fill_div(s):
196
253
    """
197
254
    CSS is stupid. In some cases we need to replace an empty value with
199
256
 
200
257
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
201
258
    """
202
 
    
203
 
 
204
259
    if s is None:
205
260
        return '&nbsp;'
206
261
    elif isinstance(s, int):
214
269
            s = s.decode('iso-8859-15')
215
270
        return s
216
271
 
 
272
HSC = HTMLStructureCleaner()
217
273
 
218
274
def fixed_width(s):
219
275
    """
230
286
            s = s.decode('utf-8')
231
287
        except UnicodeDecodeError:
232
288
            s = s.decode('iso-8859-15')
233
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
289
 
 
290
    s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
 
291
 
 
292
    return HSC.clean(s).replace('\n', '<br/>')
234
293
 
235
294
 
236
295
def fake_permissions(kind, executable):
269
328
P95_MEG = int(0.9 * MEG)
270
329
P95_GIG = int(0.9 * GIG)
271
330
 
 
331
 
272
332
def human_size(size, min_divisor=0):
273
333
    size = int(size)
274
334
    if (size == 0) and (min_divisor == 0):
303
363
    return out
304
364
 
305
365
 
 
366
def local_path_from_url(url):
 
367
    """Convert Bazaar URL to local path, ignoring readonly+ prefix"""
 
368
    readonly_prefix = 'readonly+'
 
369
    if url.startswith(readonly_prefix):
 
370
        url = url[len(readonly_prefix):]
 
371
    return urlutils.local_path_from_url(url)
 
372
 
 
373
 
306
374
def fill_in_navigation(navigation):
307
375
    """
308
376
    given a navigation block (used by the template for the page header), fill
314
382
        navigation.position = 0
315
383
    navigation.count = len(navigation.revid_list)
316
384
    navigation.page_position = navigation.position // navigation.pagesize + 1
317
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
385
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
 
386
 - 1)) // navigation.pagesize
318
387
 
319
388
    def get_offset(offset):
320
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
 
389
        if (navigation.position + offset < 0) or (
 
390
           navigation.position + offset > navigation.count - 1):
321
391
            return None
322
392
        return navigation.revid_list[navigation.position + offset]
323
393
 
330
400
            navigation.next_page_revid)
331
401
    start_revno = navigation.history.get_revno(navigation.start_revid)
332
402
 
333
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
403
    params = {'filter_file_id': navigation.filter_file_id}
334
404
    if getattr(navigation, 'query', None) is not None:
335
405
        params['q'] = navigation.query
336
406
 
345
415
            [navigation.scan_url, next_page_revno], **params)
346
416
 
347
417
 
 
418
def directory_breadcrumbs(path, is_root, view):
 
419
    """
 
420
    Generate breadcrumb information from the directory path given
 
421
 
 
422
    The path given should be a path up to any branch that is currently being
 
423
    served
 
424
 
 
425
    Arguments:
 
426
    path -- The path to convert into breadcrumbs
 
427
    is_root -- Whether or not loggerhead is serving a branch at its root
 
428
    view -- The type of view we are showing (files, changes etc)
 
429
    """
 
430
    # Is our root directory itself a branch?
 
431
    if is_root:
 
432
        breadcrumbs = [{
 
433
            'dir_name': path,
 
434
            'path': '',
 
435
            'suffix': view,
 
436
        }]
 
437
    else:
 
438
        # Create breadcrumb trail for the path leading up to the branch
 
439
        breadcrumbs = [{
 
440
            'dir_name': "(root)",
 
441
            'path': '',
 
442
            'suffix': '',
 
443
        }]
 
444
        if path != '/':
 
445
            dir_parts = path.strip('/').split('/')
 
446
            for index, dir_name in enumerate(dir_parts):
 
447
                breadcrumbs.append({
 
448
                    'dir_name': dir_name,
 
449
                    'path': '/'.join(dir_parts[:index + 1]),
 
450
                    'suffix': '',
 
451
                })
 
452
            # If we are not in the directory view, the last crumb is a branch,
 
453
            # so we need to specify a view
 
454
            if view != 'directory':
 
455
                breadcrumbs[-1]['suffix'] = '/' + view
 
456
    return breadcrumbs
 
457
 
 
458
 
 
459
def branch_breadcrumbs(path, inv, view):
 
460
    """
 
461
    Generate breadcrumb information from the branch path given
 
462
 
 
463
    The path given should be a path that exists within a branch
 
464
 
 
465
    Arguments:
 
466
    path -- The path to convert into breadcrumbs
 
467
    inv -- Inventory to get file information from
 
468
    view -- The type of view we are showing (files, changes etc)
 
469
    """
 
470
    dir_parts = path.strip('/').split('/')
 
471
    inner_breadcrumbs = []
 
472
    for index, dir_name in enumerate(dir_parts):
 
473
        inner_breadcrumbs.append({
 
474
            'dir_name': dir_name,
 
475
            'path': '/'.join(dir_parts[:index + 1]),
 
476
            'suffix': '/' + view,
 
477
        })
 
478
    return inner_breadcrumbs
 
479
 
 
480
 
348
481
def decorator(unbound):
 
482
 
349
483
    def new_decorator(f):
350
484
        g = unbound(f)
351
485
        g.__name__ = f.__name__
358
492
    return new_decorator
359
493
 
360
494
 
361
 
# common threading-lock decorator
362
 
def with_lock(lockname, debug_name=None):
363
 
    if debug_name is None:
364
 
        debug_name = lockname
365
 
    @decorator
366
 
    def _decorator(unbound):
367
 
        def locked(self, *args, **kw):
368
 
            getattr(self, lockname).acquire()
369
 
            try:
370
 
                return unbound(self, *args, **kw)
371
 
            finally:
372
 
                getattr(self, lockname).release()
373
 
        return locked
374
 
    return _decorator
375
 
 
376
495
 
377
496
@decorator
378
497
def lsprof(f):
 
498
 
379
499
    def _f(*a, **kw):
380
500
        from loggerhead.lsprof import profile
381
501
        import cPickle
382
502
        z = time.time()
383
503
        ret, stats = profile(f, *a, **kw)
384
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
 
504
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
 
505
            int((time.time() - z) * 1000)))
385
506
        stats.sort()
386
507
        stats.freeze()
387
508
        now = time.time()
388
509
        msec = int(now * 1000) % 1000
389
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
 
510
        timestr = time.strftime('%Y%m%d%H%M%S',
 
511
                                time.localtime(now)) + ('%03d' % (msec,))
390
512
        filename = f.__name__ + '-' + timestr + '.lsprof'
391
513
        cPickle.dump(stats, open(filename, 'w'), 2)
392
514
        return ret
421
543
#         for re-ordering an existing page by different sort
422
544
 
423
545
t_context = threading.local()
424
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
425
 
          'compare_revid', 'sort')
 
546
_valid = (
 
547
    'start_revid', 'filter_file_id', 'q', 'remember', 'compare_revid', 'sort')
426
548
 
427
549
 
428
550
def set_context(map):
458
580
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
459
581
 
460
582
    @classmethod
461
 
    def _turn_sigterm_into_systemexit(self):
 
583
    def _turn_sigterm_into_systemexit(cls):
462
584
        """
463
585
        Attempts to turn a SIGTERM exception into a SystemExit exception.
464
586
        """
466
588
            import signal
467
589
        except ImportError:
468
590
            return
 
591
 
469
592
        def handle_term(signo, frame):
470
593
            raise SystemExit
471
594
        signal.signal(signal.SIGTERM, handle_term)
472
595
 
473
596
    @classmethod
474
 
    def is_installed(self):
475
 
        return os.environ.get(self._reloader_environ_key)
476
 
    
 
597
    def is_installed(cls):
 
598
        return os.environ.get(cls._reloader_environ_key)
 
599
 
477
600
    @classmethod
478
 
    def install(self):
 
601
    def install(cls):
479
602
        from paste import reloader
480
603
        reloader.install(int(1))
481
 
    
482
 
    @classmethod    
483
 
    def restart_with_reloader(self):
 
604
 
 
605
    @classmethod
 
606
    def restart_with_reloader(cls):
484
607
        """Based on restart_with_monitor from paste.script.serve."""
485
608
        print 'Starting subprocess with file monitor'
486
 
        while 1:
 
609
        while True:
487
610
            args = [sys.executable] + sys.argv
488
611
            new_environ = os.environ.copy()
489
 
            new_environ[self._reloader_environ_key] = 'true'
 
612
            new_environ[cls._reloader_environ_key] = 'true'
490
613
            proc = None
491
614
            try:
492
615
                try:
493
 
                    self._turn_sigterm_into_systemexit()
 
616
                    cls._turn_sigterm_into_systemexit()
494
617
                    proc = subprocess.Popen(args, env=new_environ)
495
618
                    exit_code = proc.wait()
496
619
                    proc = None
499
622
                    return 1
500
623
            finally:
501
624
                if (proc is not None
502
 
                    and hasattr(os, 'kill')):
 
625
                    and getattr(os, 'kill', None) is not None):
503
626
                    import signal
504
627
                    try:
505
628
                        os.kill(proc.pid, signal.SIGTERM)
506
629
                    except (OSError, IOError):
507
630
                        pass
508
 
                
 
631
 
509
632
            # Reloader always exits with code 3; but if we are
510
633
            # a monitor, any exit code will restart
511
634
            if exit_code != 3:
512
635
                return exit_code
513
 
            print '-'*20, 'Restarting', '-'*20
 
 
b'\\ No newline at end of file'
 
636
            print '-'*20, 'Restarting', '-'*20
 
637
 
 
638
 
 
639
def convert_file_errors(application):
 
640
    """WSGI wrapper to convert some file errors to Paste exceptions"""
 
641
    def new_application(environ, start_response):
 
642
        try:
 
643
            return application(environ, start_response)
 
644
        except (IOError, OSError), e:
 
645
            import errno
 
646
            from paste import httpexceptions
 
647
            if e.errno == errno.ENOENT:
 
648
                raise httpexceptions.HTTPNotFound()
 
649
            elif e.errno == errno.EACCES:
 
650
                raise httpexceptions.HTTPForbidden()
 
651
            else:
 
652
                raise
 
653
    return new_application
 
654
 
 
655
 
 
656
def convert_to_json_ready(obj):
 
657
    if isinstance(obj, Container):
 
658
        d = obj.__dict__.copy()
 
659
        del d['_properties']
 
660
        return d
 
661
    elif isinstance(obj, datetime.datetime):
 
662
        return tuple(obj.utctimetuple())
 
663
    raise TypeError(repr(obj) + " is not JSON serializable")