source: main/waeup.kofa/trunk/src/waeup/kofa/utils/helpers.py @ 14480

Last change on this file since 14480 was 13537, checked in by Henrik Bettermann, 9 years ago

Catch traceback if header contains forbidden characters.

  • Property svn:keywords set to Id
File size: 26.3 KB
Line 
1## $Id: helpers.py 13537 2015-12-09 06:44:49Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""General helper functions for Kofa.
19"""
20import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
21import datetime
22import imghdr
23import logging
24import os
25import pytz
26import re
27import shutil
28import tempfile
29import grok
30from cStringIO import StringIO
31from docutils.core import publish_string
32from zope.component import getUtility
33from zope.component.interfaces import IFactory
34from zope.interface import implementedBy
35from zope.interface.interface import Method, Attribute
36from zope.schema import getFieldNames
37from zope.schema.fieldproperty import FieldProperty
38from zope.security.interfaces import NoInteraction
39from zope.security.management import getInteraction
40from zope.pluggableauth.interfaces import IAuthenticatorPlugin
41from zope.formlib.widget import renderElement
42
43BUFSIZE = 8 * 1024
44
45
46def remove_file_or_directory(filepath):
47    """Remove a file or directory.
48
49    Different to :func:`shutil.rmtree` we also accept not existing
50    paths (returning silently) and if a dir turns out to be a regular
51    file, we remove that.
52    """
53    filepath = os.path.abspath(filepath)
54    if not os.path.exists(filepath):
55        return
56    if os.path.isdir(filepath):
57        shutil.rmtree(filepath)
58    else:
59        os.unlink(filepath)
60    return
61
62
63def copy_filesystem_tree(src, dst, overwrite=False, del_old=False):
64    """Copy contents of directory src to directory dst.
65
66    Both directories must exists.
67
68    If `overwrite` is true, any same named objects will be
69    overwritten. Otherwise these files will not be touched.
70
71    If `del_old` is true, copied files and directories will be removed
72    from the src directory.
73
74    This functions returns a list of non-copied files.
75
76    Unix hidden files and directories (starting with '.') are not
77    processed by this function.
78    """
79    if not os.path.exists(src):
80        raise ValueError('source path does not exist: %s' % src)
81    if not os.path.exists(dst):
82        raise ValueError('destination path does not exist: %s' % dst)
83    if not os.path.isdir(src):
84        raise ValueError('source path is not a directory: %s' % src)
85    if not os.path.isdir(dst):
86        raise ValueError('destination path is not a directory: %s' % dst)
87    not_copied = []
88    for item in os.listdir(src):
89        if item.startswith('.'):
90            continue  # We do not copy hidden stuff...
91        itemsrc = os.path.join(src, item)
92        itemdst = os.path.join(dst, item)
93
94        if os.path.exists(itemdst):
95            if overwrite is True:
96                remove_file_or_directory(itemdst)
97            else:
98                not_copied.append(item)
99                continue
100
101        if os.path.isdir(itemsrc):
102            shutil.copytree(itemsrc, itemdst)
103        else:
104            shutil.copy2(itemsrc, itemdst)
105        if del_old:
106            remove_file_or_directory(itemsrc)
107    return not_copied
108
109
110def get_inner_HTML_part(html_code):
111    """Return the 'inner' part of a complete HTML snippet.
112
113    If there is a form part, get this.
114
115    If there is no form part, try to return the body part contents.
116
117    If there is no body, return as-is.
118
119    Let's see how that works. If we deliver some doc with form, we
120    will get that form only:
121
122       >>> doc = '<html><form>My Form</form>Outside the form</html>'
123       >>> get_inner_HTML_part(doc)
124       '<form>My Form</form>'
125
126    No form? Then seek for a body part and get the contents:
127
128       >>> doc = '<html><body>My Body</body>Trailing Trash</html>'
129       >>> get_inner_HTML_part(doc)
130       'My Body'
131
132    If none of these is included, return what we got:
133
134       >>> doc = '<html>without body nor form</html>'
135       >>> get_inner_HTML_part(doc)
136       '<html>without body nor form</html>'
137
138    """
139
140    try:
141        result = re.match('^.+(<form[^\>]*>.*</form>).+$', html_code,
142                          re.DOTALL).groups()[0]
143        return result
144    except AttributeError:
145        # No <form> part included
146        try:
147            result = re.match('^.+<body[^\>]*>(.*)</body>.*$', html_code,
148                              re.DOTALL).groups()[0]
149            return result
150        except AttributeError:
151            # No <form> and no <body> tag...
152            pass
153    return html_code
154
155
156class FactoryBase(grok.GlobalUtility):
157    """A factory for things.
158
159    This is a baseclass for easier creation of factories. Factories
160    are utilities that are registered under a certain name and return
161    instances of certain classes when called.
162
163    In :mod:`waeup.kofa` we use factories extensively for
164    batching. While processing a batch some processors looks up a
165    factory to create real-world instances that then get filled with
166    data from imported CSV files.
167
168    To get rid of reimplementing the same stuff over and over again,
169    most notably the methods defined here, we offer this base class
170    (which will *not* be registered as a factory itself).
171
172    Real factories can then be created like this:
173
174       >>> import grok
175       >>> from waeup.kofa.utils.helpers import FactoryBase
176       >>> class MyObject(object):
177       ...   # Some class we want to get instances of.
178       ...   pass
179       >>> class MyObjectFactory(FactoryBase):
180       ...   # This is the factory for MyObject instances
181       ...   grok.name(u'waeup.kofa.factory.MyObject')
182       ...   factory = MyObject
183
184    That's it. It is essential to set the ``factory`` attribute, which
185    will determine the class of which instances should be created when
186    called. The given name must even be unique amongst all utilities
187    registered during runtime. While you can pick any name you like
188    you might want to prepend ``waeup.kofa.factory.`` to the name
189    string to make sure it does not clash with names of other
190    utilities one day.
191
192    Before all this works we have to grok the baseclass once and our
193    freshly defined factory. This executes all the component
194    registration stuff we don't want to do ourselves. In daily use
195    this is done automatically on startup of a :mod:`waeup.kofa`
196    system.
197
198       >>> grok.testing.grok('waeup.kofa.utils.helpers')
199       >>> grok.testing.grok_component(
200       ...    'MyObjectFactory', MyObjectFactory
201       ...  )
202       True
203
204    After grokking we (and processors) can create objects without
205    knowing about the location of the real class definition, just by
206    the factory name:
207
208       >>> from zope.component import createObject
209       >>> obj = createObject('waeup.kofa.factory.MyObject')
210       >>> isinstance(obj, MyObject)
211       True
212
213    We can also use the regular utility lookups to find our new
214    factory:
215
216       >>> from zope.component import getUtility
217       >>> from zope.component.interfaces import IFactory
218       >>> factory = getUtility(
219       ...   IFactory, name='waeup.kofa.factory.MyObject'
220       ...   )
221       >>> isinstance(factory, MyObjectFactory)
222       True
223
224    And this factory generates `MyObject` instances:
225
226       >>> obj = factory()
227       >>> isinstance(obj, MyObject)
228       True
229
230    """
231    grok.baseclass()  # Do not grok this class, do not register us.
232    grok.implements(IFactory)
233    # You can override any of the following attributes in derived
234    # classes. The `grok.name` setting *must* even be set to some
235    # unique value.
236    grok.name(u'waeup.Factory')
237    title = u"Create instances of ``factory``.",
238    description = u"This factory instantiates new applicant instances."
239    factory = None
240
241    def __call__(self, *args, **kw):
242        """The main factory function.
243
244        Returns an instance of the requested object.
245        """
246        return self.factory()
247
248    def getInterfaces(self):
249        # Required by IFactory
250        return implementedBy(self.factory)
251
252
253def ReST2HTML_w_warnings(source_string):
254    """Convert a reStructuredText string to HTML preserving warnings.
255
256    Returns a tuple ``(<HTML_CODE>, <WARNINGS>)``, both being
257    strings. Where ``<HTML_CODE>`` is the HTML code generated from the
258    source string (in unicode), ``<WARNINGS>`` is a string containing
259    any warning messages or ``None``.
260
261    Regular multi-line ReStructuredText strings will be returned as
262    HTML code:
263
264        >>> from waeup.kofa.utils.helpers import ReST2HTML
265        >>> source = '''
266        ... Headline
267        ... ========
268        ...
269        ... - A list item
270        ... - Another item
271        ...
272        ... Thanks for watching!
273        ... '''
274        >>> html, warnings = ReST2HTML_w_warnings(source)
275        >>> print html
276        <div class="document" id="headline">
277        <h1 class="title">Headline</h1>
278        <BLANKLINE>
279        <ul class="simple">
280        <li>A list item</li>
281        <li>Another item</li>
282        </ul>
283        <p>Thanks for watching!</p>
284        </div>
285
286    Here no warnings happened, so the `warnings` are ``None``:
287
288        >>> warnings is None
289        True
290
291    If warnings happen then they can be retrieved in the returned
292    ``warnings``. We try to render an erraneous document:
293
294        >>> source = '''
295        ... Headline
296        ... ======
297        ...
298        ... Thanks for watching!
299        ... '''
300        >>> html, warnings = ReST2HTML_w_warnings(source)
301        >>> print html
302        <div class="document" id="headline">
303        <h1 class="title">Headline</h1>
304        <BLANKLINE>
305        <p>Thanks for watching!</p>
306        </div>
307
308        >>> print warnings
309        <string>:3: (WARNING/2) Title underline too short.
310        <BLANKLINE>
311        Headline
312        ======
313        <BLANKLINE>
314
315    As you can see, the warnings are not displayed inline the document
316    but can be retrieved from the returned warnings, which is a string
317    or ``None``.
318    """
319    warnings = StringIO()
320    fulldoc = publish_string(
321        source_string, writer_name='html4css1',
322        settings_overrides={
323            'report_level': 0,
324            'warning_stream': warnings,
325            })
326    warnings.seek(0)
327    warning_msgs = warnings.read()
328    if warning_msgs:
329        # Render again, this time with no warnings inline...
330        fulldoc = publish_string(
331        source_string, writer_name='html4css1',
332        settings_overrides={
333            'report_level': 10000,
334            'halt_level': 10000,
335            'warning_stream': warnings,
336            })
337    if warning_msgs == '':
338        warning_msgs = None
339    result = get_inner_HTML_part(fulldoc).strip()
340    if not isinstance(result, unicode):
341        result = result.decode('utf-8')
342    return result, warning_msgs
343
344
345def ReST2HTML(source_string):
346    """Render a string containing ReStructuredText to HTML.
347
348    Any warnings about too short headings, etc. are silently
349    discarded. Use :func:`ReST2HTML_w_warnings` if you want to get any
350    warnings.
351
352    The returned string will be unicode.
353
354    A regular document will be rendered like this:
355
356        >>> source = '''
357        ... Headline
358        ... ========
359        ...
360        ... Thanks for watching!
361        ... '''
362        >>> html = ReST2HTML(source)
363        >>> print html
364        <div class="document" id="headline">
365        <h1 class="title">Headline</h1>
366        <BLANKLINE>
367        <p>Thanks for watching!</p>
368        </div>
369
370    A document with markup problems (here: the underline is too short)
371    will look similar:
372
373        >>> source = '''
374        ... Headline
375        ... ======
376        ...
377        ... Thanks for watching!
378        ... '''
379        >>> html = ReST2HTML(source)
380        >>> print html
381        <div class="document" id="headline">
382        <h1 class="title">Headline</h1>
383        <BLANKLINE>
384        <p>Thanks for watching!</p>
385        </div>
386
387    """
388    html, warnings = ReST2HTML_w_warnings(source_string)
389    return html
390
391
392def attrs_to_fields(cls, omit=[]):
393    """Set class attributes and bind them to the data definitions
394    specified in the interface by turning the attributes into FieldProperty
395    instances.
396
397    With Python >= 2.6 we can even use this function as a class decorator.
398
399    `omit` is a list of field names that should _not_ be turned into
400    field properties. This is useful for properties and the like.
401    """
402    iface = list(implementedBy(cls))[0]
403    for field_name in getFieldNames(iface):
404        if field_name in omit:
405            continue
406        field_property = FieldProperty(iface[field_name])
407        # Set proper docstring for the API docs.
408        field_property.__doc__ = iface[field_name].title + ' (computed attribute)'
409        setattr(cls, field_name, field_property)
410    return cls
411
412
413def get_current_principal():
414    """Get the 'current' principal.
415
416    This method works without a request. Examining a request is the
417    regular (and recommended) way to get a principal involved
418    'currently'.
419
420    Use this method only if you really have no access to the current
421    request.
422
423    Returns ``None`` when no principal is involved (for instance
424    during tests).
425    """
426    try:
427        principal = getInteraction().participations[0].principal
428    except NoInteraction:
429        return None
430    except IndexError:  # No participations present
431        return None
432    return principal
433
434
435def cmp_files(file_descr1, file_descr2):
436    """Compare two files by their file descriptors.
437
438    Returns ``True`` if both are equal, ``False`` otherwise.
439    """
440    file_descr1.seek(0)
441    file_descr2.seek(0)
442    while True:
443        b1 = file_descr1.read(BUFSIZE)
444        b2 = file_descr2.read(BUFSIZE)
445        if b1 != b2:
446            return False
447        if not b1:
448            return True
449
450
451def string_from_bytes(number):
452    """Turn a number into some textual representation.
453
454      Examples:
455
456        >>> string_from_bytes(1)
457        u'1 byte(s)'
458
459        >>> string_from_bytes(1025)
460        u'1 KB'
461
462        >>> string_from_bytes(1.5 * 1024*1024)
463        u'1.50 MB'
464
465        >>> string_from_bytes(673.286 * 1024**3)
466        u'673.29 GB'
467
468    """
469    if number < 1024:
470        return u'%s byte(s)' % (str(number),)
471    elif number < 1024 ** 2:
472        return u'%s KB' % (number / 1024,)
473    elif number < 1024 ** 3:
474        return u'%.2f MB' % (number / 1024 ** 2,)
475    return u'%.2f GB' % (number / 1024 ** 3,)
476
477
478def file_size(file_like_obj):
479    """Determine file size in most effective manner.
480
481    Returns the number of bytes in a file. This function works for
482    both, real files as well as file-like objects like cStringIO based
483    'files'.
484
485    Example:
486
487      >>> from cStringIO import StringIO
488      >>> file_size(StringIO('my file content'))
489      15
490
491    Please note that this function expects the file-like object passed
492    in to be at first reading position (it does no seek(0)) and that
493    when finished the file pointer might be at end of file.
494    """
495    if hasattr(file_like_obj, 'fileno'):
496        return os.fstat(file_like_obj.fileno())[6]
497    file_like_obj.seek(0, 2)  # seek to last position in file
498    return file_like_obj.tell()
499
500
501def get_user_account(request):
502    """Return local user account.
503    """
504    principal_id = request.principal.id
505    authenticator = getUtility(IAuthenticatorPlugin, name='users')
506    account = authenticator.getAccount(principal_id)
507    return account
508
509
510def iface_names(iface, omit=[], exclude_attribs=True, exclude_methods=True):
511    """Get all attribute names of an interface.
512
513    Searches also base interfaces.
514
515    Names of fields that are pure attributes
516    (i.e. zope.interface.Attribute) or methods are excluded by
517    default.
518
519    Names of typical fields derived from zope.schema are included.
520
521    The `omit` paramter can give a list of names to exclude.
522
523    Returns an unsorted list of strings.
524    """
525    ifaces = set((iface,))
526    # Collect all interfaces (also bases) recursively
527    while True:
528        ext_ifaces = set(ifaces)
529        for iface in ext_ifaces:
530            ext_ifaces = set.union(ext_ifaces, set(iface.getBases()))
531        if ext_ifaces == ifaces:
532            # No new interfaces found, list complete
533            break
534        ifaces = ext_ifaces
535    # Collect (filtered) names of collected interfaces
536    result = []
537    for iface in ifaces:
538        for name, descr in iface.namesAndDescriptions():
539            if name in omit:
540                continue
541            if exclude_attribs and descr.__class__ is Attribute:
542                continue
543            if exclude_methods and isinstance(descr, Method):
544                continue
545            if name in result:
546                continue
547            result.append(name)
548    return result
549
550
551def get_sorted_preferred(tuples_iterable, preferred_list):
552    """Get a list of tuples (<TITLE>,<TOKEN>) with values in
553    `preferred_list` put in front.
554
555    The rest of the tuples iterable is returned in orginal order. This
556    is useful for putting default entries on top of (already sorted)
557    lists of choice values, for instance when sorting countries and
558    their code.
559
560    Sample:
561
562    We have a list of tuples with uppercase 'titles' and lowercase
563    'tokens'. This list is already sorted but we want certain values
564    of this list to show up before other values. For instance we want
565    to see the 'C' entry to come first.
566
567      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
568      ...                       ['c'])
569      (('C', 'c'), ('A', 'a'), ('B', 'b'))
570
571    i.e. the entry with 'c' as second value moved to head of result.
572
573    We can also require multiple entries at head of list:
574
575      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
576      ...                       ['b', 'c'])
577      (('B', 'b'), ('C', 'c'), ('A', 'a'))
578
579    We required the 'b' entry to come before the 'c' entry and then
580    the rest of the input list. That's what we got.
581
582    The result is returned as a tuple of tuples to keep order of values.
583    """
584    result = [None for x in preferred_list]
585    for title, code in tuples_iterable:
586        if code in preferred_list:
587            index = preferred_list.index(code)
588            result[index] = (title, code)
589        else:
590            result.append((title, code))
591    return tuple(result)
592
593
594def now(tz=None):
595    """Get current datetime in timezone of `tz`.
596
597    If `tz`, a `tzinfo` instance, is None, UTC time is returned.
598
599    `tz` should be a timezone as defined in pytz.
600    """
601    return to_timezone(datetime.datetime.utcnow(), tz=tz)
602
603
604def to_timezone(dt, tz=None):
605    """Shift datetime into timezone `tz`.
606
607    If datetime `dt` contains no `tzinfo` (i.e. it is 'naive'), it is
608    assumed to be UTC.
609
610    If no `tz` is given, shift to UTC is performed.
611
612    If `dt` is not a datetime.datetime, the input value is returned
613    unchanged.
614    """
615    if not isinstance(dt, datetime.datetime):
616        return dt
617    if tz is None:
618        tz = pytz.utc
619    if dt.tzinfo is None:
620        dt = pytz.utc.localize(dt)
621    return tz.normalize(dt.tzinfo.normalize(dt).astimezone(tz))
622
623
624def imghdr_test_fpm(h, f):
625    """FPM fileformat test.
626
627    The `fpm` fileformat is the binary fingerprint data as created by
628    `libfprint`.
629    """
630    if len(h) >= 3 and h[:3] == 'FP1':
631        return 'fpm'
632
633
634#: Add test function in stdlib's imghdr tests.
635imghdr.tests.append(imghdr_test_fpm)
636
637
638def get_fileformat(path, bytestream=None):
639    """Try to determine the file format of a given media file.
640
641    Although checks done here are not done very thoroughly, they make
642    no assumptions about the filetype by looking at its filename
643    extension or similar. Instead they check header data to comply
644    with common known rules (Magic Words).
645
646    If bytestream is not `None` the `path` is ignored.
647
648    Returns filetype as string (something like ``'jpg'``) if
649    file-format can be recognized, ``None`` else.
650
651    Tested recognized filetypes currently are `jpg`, `png`, `fpm`, and
652    `pdf`.
653
654    More filetypes (though untested in waeup.kofa) are automatically
655    recognized because we deploy the stdlib `imghdr` library. See this
656    module's docs for a complete list of filetypes recognized.
657    """
658    if path is None and bytestream is None:
659        return None
660
661    img_type = None
662    if bytestream is not None:
663        img_type = imghdr.what(path, bytestream)
664    else:
665        img_type = imghdr.what(path)
666    for name, replacement in (('jpeg', 'jpg'), ('tiff', 'tif')):
667        if img_type == name:
668            img_type = replacement
669    return img_type
670
671
672def check_pdf(bytestream, file):
673    """Tell whether a file or bytestream is a PDF file.
674
675    Works as a test/plugin for the stdlib `imghdr` library.
676    """
677    if file is not None:
678        file.seek(0)
679        bytestream = file.read(4)
680        file.seek(0)
681
682    if bytestream.startswith('%PDF'):
683        return 'pdf'
684    return None
685
686# register check_pdf as header check function with `imghdr`
687if check_pdf not in imghdr.tests:
688    imghdr.tests.append(check_pdf)
689
690
691def merge_csv_files(path1, path2):
692    """Merge two CSV files into one (appending).
693
694    CSV data from `path2` will be merged into `path1` csv file. This
695    is a bit like 'appending' data from path2 to data from path1.
696
697    The path of the resulting temporary file will be returned.
698
699    In the result file data from `path2` will always come _after_ data
700    from `path1`.
701
702    **Caution**: It is the _callers_ responsibility to remove the
703    result file (which is created by tempfile.mkstemp) after usage.
704
705    This CSV file merging copes with different column orders in both
706    CSV files and even with different column sets in both files.
707
708    Also broken/empty CSV files can be handled.
709    """
710    # sniff the col names
711    try:
712        row10 = csv.DictReader(open(path1, 'rb')).next()
713    except StopIteration:
714        row10 = dict()
715    try:
716        row20 = csv.DictReader(open(path2, 'rb')).next()
717    except StopIteration:
718        row20 = dict()
719    fieldnames = sorted(list(set(row10.keys() + row20.keys())))
720    # now read/write the real data
721    reader1 = csv.DictReader(open(path1, 'rb'))
722    reader2 = csv.DictReader(open(path2, 'rb'))
723    wp, tmp_path = tempfile.mkstemp()
724    writer = csv.DictWriter(os.fdopen(wp, 'wb'), fieldnames)
725    writer.writerow(dict((x, x) for x in fieldnames))  # header
726    for row in reader1:
727        writer.writerow(row)
728    for row in reader2:
729        writer.writerow(row)
730    return tmp_path
731
732
733def product(sequence, start=1):
734    """Returns the product of a sequence of numbers (_not_ strings)
735    multiplied by the parameter `start` (defaults to 1). If the
736    sequence is empty, returns 0.
737    """
738    if not len(sequence):
739        return 0
740    result = start
741    for item in sequence:
742        result *= item
743    return result
744
745
746class NullHandler(logging.Handler):
747    """A logging NullHandler.
748
749    Does not log anything. Useful if you want to shut up a log.
750
751    Defined here for backwards compatibility with Python < 2.7.
752    """
753    def emit(self, record):
754        pass
755
756
757def check_csv_charset(iterable):
758    """Check contents of `iterable` regarding valid CSV encoding.
759
760    `iterable` is expected to be an iterable on _rows_ (not
761    chars). This is true for instance for
762    filehandlers. `zope.publisher.browser.FileUpload` instances are
763    _not_ iterable, unfortunately.
764
765    Returns line num of first illegal char or ``None``. Line nums
766    start counting with 1 (not zero).
767    """
768    linenum = 1
769    try:
770        reader = csv.DictReader(iterable)
771        for row in reader:
772            linenum += 1
773    except UnicodeDecodeError:
774        return linenum
775    except:
776        return linenum + 1
777    return None
778
779
780class MemInfo(dict):
781    """A dict with access to its items like if they are attributes.
782    """
783    __getattr__ = dict.__getitem__
784    __setattr__ = dict.__setitem__
785    __delattr__ = dict.__delitem__
786
787
788def get_meminfo(src="/proc/meminfo"):
789    """Get local memory info as provided in /proc/meminfo.
790
791    Entries in /proc/meminfo are available as MemInfo attributes.
792
793    By default we lookup a file /proc/meminfo. Another path can be
794    lines = open(src, 'r').read()passed in as `src` parameter. In this
795    case `src` must be a regular file and contain meminfo-style data.
796
797    If the given `src` (or `/proc/meminfo`) are not available, `None`
798    lines = open(src, 'r').read()is returned.
799    """
800    if not os.path.isfile(src):
801        return None
802    lines = open(src, 'r').read().splitlines()
803    result = MemInfo()
804    for line in lines:
805        key, value = line.split(':', 1)
806        value = int(value.split(' kB', 1)[0])
807        result[key] = value
808    return result
809
810def html2dict(value=None,portal_language='en'):
811    """Transforms a localized HTML text string into a dictionary.
812
813    Different languages must be separated by ``>>xy<<`` whereas
814    xy is the language code. Text parts without correct leading
815    language separator - usually the first part has no language
816    descriptor - are interpreted as texts in the portal's language.
817    """
818    try:
819        parts = value.split('>>')
820    except:
821        return {}
822    elements = {}
823    lang = portal_language
824    for part in parts:
825        if part[2:4] == u'<<':
826            lang = str(part[0:2].lower())
827            text = part[4:]
828            elements[lang] = renderElement(u'div id="html"',
829                contents=text)
830        else:
831            text = part
832            elements[lang] = renderElement(u'div id="html"',
833                contents=text)
834    return elements
835
836def rest2dict(value=None,portal_language='en'):
837    """Transforms a localized REST text string into a dictionary.
838
839    Different languages must be separated by ``>>xy<<``` whereas
840    xy is the language code. Text parts without correct leading
841    language separator - usually the first part has no language
842    descriptor - are interpreted as texts in the portal's language.
843    """
844    try:
845        parts = value.split('>>')
846    except:
847        return {}
848    elements = {}
849    lang = portal_language
850    for part in parts:
851        if part[2:4] == u'<<':
852            lang = str(part[0:2].lower())
853            text = part[4:]
854            elements[lang] = renderElement(u'div id="rest"',
855                contents=ReST2HTML(text))
856        else:
857            text = part
858            elements[lang] = renderElement(u'div id="rest"',
859                contents=ReST2HTML(text))
860    return elements
Note: See TracBrowser for help on using the repository browser.