""" ``model`` module.
"""
from datetime import date, datetime, time
from decimal import Decimal
from gettext import NullTranslations
from time import strptime
from wheezy.validation.i18n import (
decimal_separator,
default_date_input_format,
default_datetime_input_format,
default_time_input_format,
fallback_date_input_formats,
fallback_datetime_input_formats,
fallback_time_input_formats,
thousands_separator,
)
from wheezy.validation.patches import patch_strptime_cache_size
if not patch_strptime_cache_size(): # pragma: nocover
from warnings import warn
warn("Failed to patch _strptime._CACHE_MAX_SIZE")
del warn
del patch_strptime_cache_size
null_translations = NullTranslations()
[docs]def try_update_model(model, values, results, translations=None):
"""Try update `model` with `values` (a dict of lists or strings),
any errors encountered put into `results` and use `translations`
for i18n.
"""
if translations is None:
translations = null_translations
gettext = translations.gettext
if hasattr(model, "__iter__"):
attribute_names = model
model_type = type(model)
getter = model_type.__getitem__
setter = model_type.__setitem__
else:
attribute_names = list(model.__dict__)
attribute_names.extend(
[name for name in model.__class__.__dict__ if name[:1] != "_"]
)
getter = getattr
setter = setattr
succeed = True
for name in attribute_names:
if name not in values:
continue
value = values[name]
attr = getter(model, name)
# Check if we have a deal with list like attribute
if hasattr(attr, "__setitem__"):
# Guess type of list by checking the first item,
# fallback to str provider that leaves value unchanged.
if attr:
provider_name = type(attr[0]).__name__
if provider_name in value_providers:
value_provider = value_providers[provider_name]
else: # pragma: nocover
continue
else:
value_provider = value_providers["str"]
items = []
try:
for item in value:
items.append(value_provider(item, gettext))
attr[:] = items
except (ArithmeticError, ValueError):
results[name] = [
gettext("Multiple input was not in a correct format.")
]
succeed = False
else: # A simple value attribute
provider_name = type(attr).__name__
if provider_name in value_providers:
value_provider = value_providers[provider_name]
if isinstance(value, list):
value = value and value[-1] or ""
try:
value = value_provider(value, gettext)
setter(model, name, value)
except (ArithmeticError, ValueError):
results[name] = [
gettext("Input was not in a correct format.")
]
succeed = False
return succeed
# region: internal details
# value_provider => lambda value, gettext: parsed_value
[docs]def bytes_value_provider(value, gettext):
"""Converts ``value`` to ``bytes``."""
if value is None:
return None
t = type(value)
if t is bytes:
return value
if t is str:
return value.encode("UTF-8")
return str(value).encode("UTF-8")
[docs]def str_value_provider(value, gettext):
"""Converts ``value`` to ``str``."""
if value is None:
return None
t = type(value)
if t is str:
return value.strip()
if t is bytes:
return value.strip().decode("UTF-8")
return str(value)
[docs]def int_value_provider(value, gettext):
"""Converts ``value`` to ``int``."""
if value is None or type(value) is int:
return value
value = str(value).strip()
if value:
s = thousands_separator(gettext)
if s in value:
value = value.replace(s, "")
return int(value)
else:
return None
decimal_zero = Decimal(0)
decimal_zero_values = ["0", "0.0", "0.00"]
[docs]def decimal_value_provider(value, gettext):
"""Converts ``value`` to ``Decimal``."""
if value is None:
return None
value = str(value).strip()
if value:
s = thousands_separator(gettext)
if s in value:
value = value.replace(s, "")
s = decimal_separator(gettext)
if s in value:
value = value.replace(s, ".", 1)
if value in decimal_zero_values:
return decimal_zero
return Decimal(value)
else:
return None
boolean_true_values = ["1", "True"]
[docs]def bool_value_provider(value, gettext):
"""Converts ``value`` to ``bool``."""
if value is None or type(value) is bool:
return value
value = str(value).strip()
return value in boolean_true_values
[docs]def float_value_provider(value, gettext):
"""Converts ``value`` to ``float``."""
if value is None or type(value) is float:
return value
value = str(value).strip()
if value:
s = thousands_separator(gettext)
if s in value:
value = value.replace(s, "")
s = decimal_separator(gettext)
if s in value:
value = value.replace(s, ".", 1)
return float(value)
else:
return None
[docs]def date_value_provider(value, gettext):
"""Converts ``value`` to ``datetime.date``."""
if value is None:
return None
value = str(value).strip()
if value:
try:
return date(
*strptime(value, default_date_input_format(gettext))[:3]
)
except ValueError:
for fmt in fallback_date_input_formats(gettext).split("|"):
try:
return date(*strptime(value, fmt)[:3])
except ValueError:
continue
raise ValueError()
else:
return None
[docs]def time_value_provider(value, gettext):
"""Converts ``value`` to ``datetime.time``."""
if value is None:
return None
value = str(value).strip()
if value:
try:
return time(
*strptime(value, default_time_input_format(gettext))[3:6]
)
except ValueError:
for fmt in fallback_time_input_formats(gettext).split("|"):
try:
return time(*strptime(value, fmt)[3:6])
except ValueError:
continue
raise ValueError()
else:
return None
[docs]def datetime_value_provider(value, gettext):
"""Converts ``value`` to ``datetime.datetime``."""
if value is None:
return None
value = str(value).strip()
if value:
try:
return datetime(
*strptime(value, default_datetime_input_format(gettext))[:6]
)
except ValueError:
for fmt in fallback_datetime_input_formats(gettext).split("|"):
try:
return datetime(*strptime(value, fmt)[:6])
except ValueError:
continue
value = date_value_provider(value, gettext)
return datetime(value.year, value.month, value.day)
else:
return None
value_providers = {
"int": int_value_provider,
"Decimal": decimal_value_provider,
"bool": bool_value_provider,
"float": float_value_provider,
"date": date_value_provider,
"time": time_value_provider,
"datetime": datetime_value_provider,
}
value_providers["str"] = str_value_provider
value_providers["bytes"] = bytes_value_provider