# -*- coding: utf-8 -*-
u"""Helper functions.
.. :module: helpers
:synopsis: Helper functions.
"""
from collections import defaultdict
import datetime
from lxml import etree
import dateutil.parser
__all__ = [
'to_bool',
'to_date',
'to_datetime',
'to_float',
'to_int',
'to_str',
'to_time',
'from_bool',
'from_date',
'from_datetime',
'from_float',
'from_int',
'from_str',
'from_time',
'CAST_DICT',
'normalize_tag',
'etree_to_dict',
'dict_to_etree',
]
def no_empty_value(func):
"""Raises an exception if function argument is empty."""
def wrapper(value):
if not value:
raise Exception("Empty value not allowed")
return func(value)
return wrapper
[docs]def to_bool(value):
cases = {
'0': False,
'false': False,
'NO': False,
'1': True,
'true': True,
'YES': True,
}
return cases.get(value, bool(value))
[docs]def to_str(value):
u"""Represents values as unicode strings to support diacritics."""
return unicode(value)
[docs]def to_int(value):
return int(value)
[docs]def to_float(value):
return float(value)
@no_empty_value
[docs]def to_time(value):
value = str(value)
# dateutil.parse has problems parsing full hours without minutes
sep = value[2:3]
if not (sep == ':' or sep.isdigit()):
value = value[:2] + ':00' + value[2:]
return dateutil.parser.parse(value).time()
@no_empty_value
[docs]def to_datetime(value):
value = str(value)
return dateutil.parser.parse(value)
@no_empty_value
[docs]def to_date(value):
value = str(value)
return dateutil.parser.parse(value)
[docs]def from_bool(value):
cases = {
True: 'YES',
False: 'NO',
}
try:
return cases.get(value, bool(value))
except Exception:
return False
[docs]def from_str(value):
return str(value)
[docs]def from_int(value):
return str(value)
[docs]def from_float(value):
return str(value)
[docs]def from_time(value):
if not isinstance(value, datetime.time):
raise Exception("Value %r is not datetime.time object" % value)
return value.isoformat()
@no_empty_value
[docs]def from_datetime(value):
if not isinstance(value, datetime.datetime):
raise Exception("Unexpected type %s of value %s (expected datetime.datetime)" % (type(value), repr(value)))
if value.tzinfo is None:
value = value.replace(tzinfo=dateutil.tz.tzlocal())
return value.replace(microsecond=0).isoformat()
@no_empty_value
[docs]def from_date(value):
if not isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
raise Exception(u"Not datetime.date object but %s: %s" % (type(value), repr(value)))
return value.isoformat()
CAST_DICT = {
bool: from_bool,
int: from_int,
str: from_str,
float: from_float,
datetime.time: from_time,
datetime.datetime: from_datetime,
datetime.date: from_date,
}
[docs]def normalize_tag(tag):
u"""Normalizes tag name.
>>> normalize_tag('tag-NaMe')
'tag_name'
"""
return tag.lower().replace('-', '_')
[docs]def etree_to_dict(t):
u"""Converts an lxml.etree object to Python dict.
>>> etree_to_dict(etree.Element('root'))
{'root': None}
:param etree.Element t: lxml tree to convert
:returns d: a dict representing the lxml tree ``t``
:rtype: dict
"""
d = {t.tag: {} if t.attrib else None}
children = list(t)
if children:
dd = defaultdict(list)
for dc in map(etree_to_dict, children):
for k, v in dc.iteritems():
dd[k].append(v)
d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.iteritems()}}
if t.attrib:
d[t.tag].update(('@' + k, v) for k, v in t.attrib.iteritems())
if t.text:
text = t.text.strip()
if children or t.attrib:
if text:
d[t.tag]['#text'] = text
else:
d[t.tag] = text
return d
[docs]def dict_to_etree(d, root):
u"""Converts a dict to lxml.etree object.
>>> dict_to_etree({'root': {'#text': 'node_text', '@attr': 'val'}}, etree.Element('root'))
'<root><root attr="val">node_text</root></root>'
:param dict d: dict representing the XML tree
:param etree.Element root: XML node which will be assigned the resulting tree
:returns: Textual representation of the XML tree
:rtype: str
"""
def _to_etree(d, node):
if d is None or len(d) == 0:
pass
elif isinstance(d, basestring):
node.text = d
elif isinstance(d, dict):
for k, v in d.items():
assert isinstance(k, basestring)
if k.startswith('#'):
assert k == '#text' and isinstance(v, basestring)
node.text = v
elif k.startswith('@'):
assert isinstance(v, basestring)
node.set(k[1:], v)
elif isinstance(v, list):
# No matter the child count, their parent will be the same.
sub_elem = etree.SubElement(node, k)
for child_num, e in enumerate(v):
if e is None:
if child_num == 0:
# Found the first occurrence of an empty child,
# skip creating of its XML repr, since it would be
# the same as ``sub_element`` higher up.
continue
# A list with None element means an empty child node
# in its parent, thus, recreating tags we have to go
# up one level.
# <node><child/></child></node> <=> {'node': 'child': [None, None]}
_to_etree(node, k)
else:
# If this isn't first child and it's a complex
# value (dict), we need to check if it's value
# is equivalent to None.
if child_num != 0 and not (isinstance(e, dict) and not all(e.values())):
# At least one child was None, we have to create
# a new parent-node, which will not be empty.
sub_elem = etree.SubElement(node, k)
_to_etree(e, sub_elem)
else:
_to_etree(v, etree.SubElement(node, k))
elif etree.iselement(d):
# Supports the case, when we got an empty child and want to recreate it.
etree.SubElement(d, node)
else:
raise AttributeError('Argument is neither dict nor basestring.')
_to_etree(d, root)
return etree.tostring(root)