mappet/helpers.py
# -*- coding: utf-8 -*-
u"""Helper functions.
.. :module: helpers
:synopsis: Helper functions.
"""
from collections import defaultdict
from decimal import Decimal
from functools import partial, wraps
import datetime
from lxml import etree
import dateutil.parser
__all__ = [
'to_bool',
'to_date',
'to_datetime',
'to_decimal',
'to_float',
'to_int',
'to_str',
'to_time',
'from_bool',
'from_date',
'from_datetime',
'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."""
@wraps(func)
def wrapper(value):
if not value:
raise Exception("Empty value not allowed")
return func(value)
return wrapper
def to_bool(value):
"""Converts human boolean-like values to Python boolean.
Falls back to :class:`bool` when ``value`` is not recognized.
:param value: the value to convert
:returns: ``True`` if value is truthy, ``False`` otherwise
:rtype: bool
"""
cases = {
'0': False,
'false': False,
'no': False,
'1': True,
'true': True,
'yes': True,
}
value = value.lower() if isinstance(value, basestring) else value
return cases.get(value, bool(value))
def to_str(value):
u"""Represents values as unicode strings to support diacritics."""
return unicode(value)
def to_int(value):
return int(value)
def to_float(value):
return float(value)
def to_decimal(value):
return Decimal(value)
@no_empty_value
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
def to_datetime(value):
return parse_datetime(value)
def parse_datetime(value):
value = str(value)
return dateutil.parser.parse(value)
@no_empty_value
def to_date(value):
return parse_datetime(value)
def from_bool(value):
cases = {
True: 'YES',
False: 'NO',
}
try:
return cases.get(value, bool(value))
except Exception:
return False
def from_time(value):
if not isinstance(value, datetime.time):
raise Exception('Value {} is not datetime.time object'.format(value))
return value.isoformat()
@no_empty_value
def from_datetime(value):
if not isinstance(value, datetime.datetime):
raise Exception('Unexpected type {} of value {} (expected datetime.datetime)'.format(type(value), value))
if value.tzinfo is None:
value = value.replace(tzinfo=dateutil.tz.tzlocal()) # pragma: nocover
return value.replace(microsecond=0).isoformat()
@no_empty_value
def from_date(value):
if not isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
raise Exception('Not datetime.date object but {}: {}'.format(type(value), value))
return value.isoformat()
CAST_DICT = {
bool: from_bool,
int: str,
str: str,
unicode: str,
float: str,
datetime.time: from_time,
datetime.datetime: from_datetime,
datetime.date: from_date,
}
def normalize_tag(tag):
u"""Normalizes tag name.
:param str tag: tag name to normalize
:rtype: str
:returns: normalized tag name
>>> normalize_tag('tag-NaMe')
'tag_name'
"""
return tag.lower().replace('-', '_')
def etree_to_dict(t, trim=True, **kw):
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)
etree_to_dict_w_args = partial(etree_to_dict, trim=trim, **kw)
if children:
dd = defaultdict(list)
d = {t.tag: {}}
for dc in map(etree_to_dict_w_args, children):
for k, v in dc.iteritems():
# do not add Comment instance to the key
if k is not etree.Comment:
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 trim and t.text:
t.text = t.text.strip()
if t.text:
if t.tag is etree.Comment and not kw.get('without_comments'):
# adds a comments node
d['#comments'] = t.text
elif children or t.attrib:
d[t.tag]['#text'] = t.text
else:
d[t.tag] = t.text
return d
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')) # doctest: +ELLIPSIS
<Element root at 0x...>
: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:
return
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'
assert 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 root