Source code for dirty_models.fields

"""
Fields to be used with dirty models.
"""

from datetime import date, datetime, time, timedelta

from collections import Mapping
from dateutil.parser import parse as dateutil_parse
from enum import Enum
from functools import wraps

from .model_types import ListModel

__all__ = ['IntegerField', 'FloatField', 'BooleanField', 'StringField', 'StringIdField',
           'TimeField', 'DateField', 'DateTimeField', 'TimedeltaField', 'ModelField', 'ArrayField',
           'HashMapField', 'BlobField', 'MultiTypeField', 'EnumField']


class BaseField:
    """Base field descriptor."""

    def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=False, default=None, doc=None):
        self._name = None
        self.name = name
        self.alias = alias
        self.read_only = read_only
        self.default = default
        self._getter = getter
        self._setter = setter
        self.__doc__ = doc or self.get_field_docstring()

    def get_field_docstring(self):
        dcstr = '{0} field'.format(self.__class__.__name__)
        if self.read_only:
            dcstr += ' [READ ONLY]'
        return dcstr

    def export_definition(self):
        return {'name': self.name,
                'alias': self.alias,
                'read_only': self.read_only,
                'doc': self.__doc__}

    @property
    def name(self):
        """Name getter: Field name or field alias that it will be set."""
        return self._name

    @name.setter
    def name(self, name):
        """Name setter: Field name or field alias that it will be set."""
        self._name = name

    def use_value(self, value):
        """Converts value to field type or use original"""
        if self.check_value(value):
            return value
        return self.convert_value(value)

    def convert_value(self, value):
        """Converts value to field type"""
        return value

    def check_value(self, value):
        """Checks whether value is field's type"""
        return False

    def can_use_value(self, value):
        """Checks whether value could be converted to field's type"""
        return True

    def set_value(self, obj, value):
        """Sets value to model"""
        obj.set_field_value(self.name, value)

    def get_value(self, obj):
        """Gets value from model"""
        return obj.get_field_value(self.name)

    def delete_value(self, obj):
        """Removes field value from model"""
        obj.delete_field_value(self.name)

    def _check_name(self):
        if self._name is None:
            raise AttributeError("Field name must be set")

    def __get__(self, obj, cls=None):
        if obj is None:
            return self

        self._check_name()

        if self._getter:
            return self._getter(self, obj, cls)

        return self.get_value(obj)

    def __set__(self, obj, value):
        self._check_name()

        if self._setter:
            self._setter(self, obj, value)
            return

        from dirty_models.utils import Factory

        def set_value(v):
            if value is None:
                self.delete_value(obj)
            elif self.check_value(v) or self.can_use_value(v):
                self.set_value(obj, self.use_value(v))
            elif isinstance(value, Factory):
                set_value(v())

        set_value(value)

    def __delete__(self, obj):
        self._check_name()
        self.delete_value(obj)


def can_use_enum(func):
    """
    Decorator to use Enum value on type checks.
    """

    @wraps(func)
    def inner(self, value):
        if isinstance(value, Enum):
            return self.check_value(value.value) or func(self, value.value)

        return func(self, value)

    return inner


def convert_enum(func):
    """
    Decorator to use Enum value on type casts.
    """

    @wraps(func)
    def inner(self, value):
        try:
            if self.check_value(value.value):
                return value.value
            return func(self, value.value)
        except AttributeError:
            pass

        return func(self, value)

    return inner


[docs]class IntegerField(BaseField): """ It allows to use an integer as value in a field. **Automatic cast from:** * :class:`float` * :class:`str` if all characters are digits * :class:`~enum.Enum` if value of enum can be cast. """
[docs] @convert_enum def convert_value(self, value): if isinstance(value, str): return int(value, 0)
return int(value)
[docs] def check_value(self, value):
return isinstance(value, int)
[docs] @can_use_enum def can_use_value(self, value): if isinstance(value, float): return True elif isinstance(value, str): try: int(value, 0) return True except ValueError: pass
return False
[docs]class FloatField(BaseField): """ It allows to use a float as value in a field. **Automatic cast from:** * :class:`int` * :class:`str` if all characters are digits and there is only one dot (``.``). * :class:`~enum.Enum` if value of enum can be cast. """
[docs] @convert_enum def convert_value(self, value):
return float(value)
[docs] def check_value(self, value):
return isinstance(value, float)
[docs] @can_use_enum def can_use_value(self, value): return isinstance(value, int) \ or (isinstance(value, str) and
value.replace('.', '', 1).isnumeric())
[docs]class BooleanField(BaseField): """ It allows to use a boolean as value in a field. **Automatic cast from:** * :class:`int` ``0`` become ``False``, anything else ``True`` * :class:`str` ``true`` and ``yes`` become ``True``, anything else ``False``. It is case-insensitive. * :class:`~enum.Enum` if value of enum can be cast. """
[docs] @convert_enum def convert_value(self, value): if isinstance(value, str): if value.lower().strip() in ['true', 'yes']: return True else: return False
return bool(value)
[docs] def check_value(self, value):
return isinstance(value, bool)
[docs] @can_use_enum def can_use_value(self, value):
return isinstance(value, (int, str))
[docs]class StringField(BaseField): """ It allows to use a string as value in a field. **Automatic cast from:** * :class:`int` * :class:`float` * :class:`~enum.Enum` if value of enum can be cast. """
[docs] @convert_enum def convert_value(self, value):
return str(value)
[docs] def check_value(self, value):
return isinstance(value, str)
[docs] @can_use_enum def can_use_value(self, value):
return isinstance(value, (int, float))
[docs]class StringIdField(StringField): """ It allows to use a string as value in a field, but not allows empty strings. Empty string are like ``None`` and they will remove data of field. **Automatic cast from:** * :class:`int` * :class:`float` * :class:`~enum.Enum` if value of enum can be cast. """
[docs] def set_value(self, obj, value): """Sets value to model if not empty""" if value: obj.set_field_value(self.name, value) else:
self.delete_value(obj) class DateTimeBaseField(BaseField): """Base field for time or/and date fields.""" date_parsers = {} def __init__(self, parse_format=None, **kwargs): """ :param parse_format: String format to cast string to datetime. It could be an string format or a :class:`dict` with two keys: * ``parser`` key to set how string must be parsed. It could be a callable. * ``formatter`` key to set how datetime must be formatted. It could be a callable. :type parse_format: str or dict """ super(DateTimeBaseField, self).__init__(**kwargs) self.parse_format = parse_format def export_definition(self): result = super(DateTimeBaseField, self).export_definition() result['parse_format'] = self.parse_format return result def get_parsed_value(self, value): """ Helper to cast string to datetime using :member:`parse_format`. :param value: String representing a datetime :type value: str :return: datetime """ def get_parser(parser_desc): try: return parser_desc['parser'] except TypeError: try: return get_parser(self.date_parsers[parser_desc]) except KeyError: return parser_desc except KeyError: pass parser = get_parser(self.parse_format) if parser is None: try: return dateutil_parse(value) except ValueError: return None if callable(parser): return parser(value) return datetime.strptime(value, parser) def get_formatted_value(self, value): """ Returns a string from datetime using :member:`parse_format`. :param value: Datetime to cast to string :type value: datetime :return: str """ def get_formatter(parser_desc): try: return parser_desc['formatter'] except TypeError: if isinstance(parser_desc, str): try: return get_formatter(self.date_parsers[parser_desc]) except KeyError: return parser_desc else: pass except KeyError: try: if isinstance(parser_desc['parser'], str): return parser_desc['parser'] except KeyError: pass formatter = get_formatter(self.parse_format) if formatter is None: return str(value) if callable(formatter): return formatter(value) return value.strftime(format=formatter)
[docs]class TimeField(DateTimeBaseField): """ It allows to use a time as value in a field. **Automatic cast from:** * :class:`list` items will be used to construct :class:`~datetime.time` object as arguments. * :class:`dict` items will be used to construct :class:`~datetime.time` object as keyword arguments. * :class:`str` will be parsed using a function or format in ``parser`` constructor parameter. * :class:`int` will be used as timestamp. * :class:`~datetime.datetime` will get time part. * :class:`~enum.Enum` if value of enum can be cast. """ def __init__(self, parse_format=None, default_timezone=None, **kwargs): """ :param parse_format: String format to cast string to datetime. It could be an string format or a :class:`dict` with two keys: * ``parser`` key to set how string must be parsed. It could be a callable. * ``formatter`` key to set how datetime must be formatted. It could be a callable. :type parse_format: str or dict :param default_timezone: Default timezone to use when value does not have one. :type default_timezone: datetime.tzinfo """ super(TimeField, self).__init__(parse_format=parse_format, **kwargs) self.default_timezone = default_timezone
[docs] @convert_enum def convert_value(self, value): if isinstance(value, list): return time(*value) elif isinstance(value, dict): return time(**value) elif isinstance(value, int): return self.convert_value(datetime.fromtimestamp(value, tz=self.default_timezone)) elif isinstance(value, str): try: if not self.parse_format: value = dateutil_parse(value) return value.time() return self.convert_value(self.get_parsed_value(value)) except Exception: return None elif isinstance(value, datetime):
return value.timetz()
[docs] def check_value(self, value):
return isinstance(value, time)
[docs] @can_use_enum def can_use_value(self, value):
return isinstance(value, (int, str, datetime, list, dict))
[docs] def set_value(self, obj, value: time): if self.default_timezone and value.tzinfo is None: value = value.replace(tzinfo=self.default_timezone)
super(TimeField, self).set_value(obj, value) def export_definition(self): result = super(TimeField, self).export_definition() if self.default_timezone: result['default_timezone'] = self.default_timezone
return result
[docs]class DateField(DateTimeBaseField): """ It allows to use a date as value in a field. **Automatic cast from:** * :class:`list` items will be used to construct :class:`~datetime.date` object as arguments. * :class:`dict` items will be used to construct :class:`~datetime.date` object as keyword arguments. * :class:`str` will be parsed using a function or format in ``parser`` constructor parameter. * :class:`int` will be used as timestamp. * :class:`~datetime.datetime` will get date part. * :class:`~enum.Enum` if value of enum can be cast. """
[docs] @convert_enum def convert_value(self, value): if isinstance(value, list): return date(*value) elif isinstance(value, dict): return date(**value) elif isinstance(value, int): return self.convert_value(datetime.fromtimestamp(value)) elif isinstance(value, str): try: if not self.parse_format: value = dateutil_parse(value) return value.date() return self.convert_value(self.get_parsed_value(value)) except Exception: return None elif isinstance(value, datetime):
return value.date()
[docs] def check_value(self, value):
return type(value) is date
[docs] @can_use_enum def can_use_value(self, value):
return isinstance(value, (int, str, datetime, list, dict))
[docs]class DateTimeField(DateTimeBaseField): """ It allows to use a datetime as value in a field. **Automatic cast from:** * :class:`list` items will be used to construct :class:`~datetime.datetime` object as arguments. * :class:`dict` items will be used to construct :class:`~datetime.datetime` object as keyword arguments. * :class:`str` will be parsed using a function or format in ``parser`` constructor parameter. * :class:`int` will be used as timestamp. * :class:`~datetime.date` will set date part. * :class:`~enum.Enum` if value of enum can be cast. """ def __init__(self, parse_format=None, default_timezone=None, force_timezone=False, **kwargs): """ :param parse_format: String format to cast string to datetime. It could be an string format or a :class:`dict` with two keys: * ``parser`` key to set how string must be parsed. It could be a callable. * ``formatter`` key to set how datetime must be formatted. It could be a callable. :type parse_format: str or dict :param default_timezone: Default timezone to use when value does not have one. :type default_timezone: datetime.tzinfo :param force_timezone: If it is True value will be converted to timezone defined on ``default_timezone`` parameter. It ``default_timezone`` is not defined it is ignored. :type: bool """ super(DateTimeField, self).__init__(parse_format=parse_format, **kwargs) self.default_timezone = default_timezone self.force_timezone = force_timezone
[docs] @convert_enum def convert_value(self, value): if isinstance(value, list): return datetime(*value) elif isinstance(value, dict): return datetime(**value) elif isinstance(value, int): return datetime.fromtimestamp(value, tz=self.default_timezone) elif isinstance(value, str): try: if not self.parse_format: return dateutil_parse(value) return self.get_parsed_value(value) except Exception: return None elif isinstance(value, date): return datetime(year=value.year, month=value.month,
day=value.day)
[docs] def check_value(self, value):
return type(value) is datetime
[docs] @can_use_enum def can_use_value(self, value):
return isinstance(value, (int, str, date, dict, list))
[docs] def set_value(self, obj, value): if self.default_timezone: if value.tzinfo is None: value = value.replace(tzinfo=self.default_timezone) elif self.force_timezone and value.tzinfo != self.default_timezone: value = value.astimezone(tz=self.default_timezone)
super(DateTimeField, self).set_value(obj, value) def export_definition(self): result = super(DateTimeField, self).export_definition() if self.default_timezone: result['default_timezone'] = self.default_timezone result['force_timezone'] = self.force_timezone
return result
[docs]class TimedeltaField(BaseField): """ It allows to use a timedelta as value in a field. **Automatic cast from:** * :class:`float` as seconds. * :class:`int` as seconds. * :class:`~enum.Enum` if value of enum can be cast. """
[docs] @convert_enum def convert_value(self, value): if isinstance(value, (int, float)):
return timedelta(seconds=value)
[docs] def check_value(self, value):
return type(value) is timedelta
[docs] @can_use_enum def can_use_value(self, value):
return isinstance(value, (int, float))
[docs]class ModelField(BaseField): """ It allows to use a model as value in a field. Model type must be defined on constructor using param model_class. If it is not defined self model will be used. It means model inside field will be the same class than model who define field. **Automatic cast from:** * :class:`dict`. * :class:`collections.Mapping`. """ def __init__(self, model_class=None, **kwargs): self._model_class = None self.model_class = model_class self._model_setter = None if 'setter' in kwargs: self._model_setter = kwargs['setter'] del (kwargs['setter']) super(ModelField, self).__init__(**kwargs) def export_definition(self): result = super(ModelField, self).export_definition() result['model_class'] = self.model_class return result def get_field_docstring(self): dcstr = super(ModelField, self).get_field_docstring() if self.model_class: dcstr += ' (:class:`{0}`)'.format('.'.join([self.model_class.__module__, self.model_class.__name__])) return dcstr @property def model_class(self): """Model_class getter: model class used on field""" return self._model_class @model_class.setter def model_class(self, model_class): """Model_class setter: model class used on field""" self._model_class = model_class
[docs] def convert_value(self, value):
return self._model_class(value)
[docs] def check_value(self, value):
return isinstance(value, self._model_class)
[docs] def can_use_value(self, value):
return isinstance(value, (dict, Mapping)) def __set__(self, obj, value): if self._model_setter: self._model_setter(self, obj, value) return if self._name is None: raise AttributeError("Field name must be set") original = self.get_value(obj) if original is None: super(ModelField, self).__set__(obj, value) elif self.check_value(value): original.clear() original.import_data(value.export_data()) elif self.can_use_value(value):
original.import_data(value) class InnerFieldTypeMixin: def __init__(self, field_type=None, **kwargs): self._field_type = None if isinstance(field_type, tuple): field_type = field_type[0](**field_type[1]) self.field_type = field_type if field_type else BaseField() super(InnerFieldTypeMixin, self).__init__(**kwargs) def export_definition(self): result = super(InnerFieldTypeMixin, self).export_definition() result['field_type'] = (self.field_type.__class__, self.field_type.export_definition()) return result @property def field_type(self): """field_type getter: field type used on array or hashmap""" return self._field_type @field_type.setter def field_type(self, value): """Model_class setter: field type used on array or hashmap""" self._field_type = value
[docs]class ArrayField(InnerFieldTypeMixin, BaseField): """ It allows to create a ListModel (iterable in :mod:`dirty_models.types`) of different elements according to the specified field_type. So it is possible to have a list of Integers, Strings, Models, etc. When using a model with no specified model_class the model inside field. **Automatic cast from:** * :class:`set`. * :class:`tuple`. """ def __init__(self, autolist=False, **kwargs): self._autolist = autolist super(ArrayField, self).__init__(**kwargs) def get_field_docstring(self): if self.field_type: return 'Array of {0}'.format(self.field_type.get_field_docstring())
[docs] def convert_value(self, value): def convert_element(element): """ Helper to convert a single item """ if not self.field_type.check_value(element) and self._field_type.can_use_value(element): return self.field_type.convert_value(element) return element if isinstance(value, (set, list, tuple, ListModel)): return ListModel([convert_element(element) for element in value], field_type=self.field_type) elif self.autolist:
return ListModel([convert_element(value)], field_type=self.field_type)
[docs] def check_value(self, value): if not isinstance(value, ListModel) or not isinstance(value.get_field_type(), type(self.field_type)): return False
return True
[docs] def can_use_value(self, value): if isinstance(value, (set, list, tuple, ListModel)): if len(value) == 0: return True for item in value: if self.field_type.can_use_value(item) or self.field_type.check_value(item): return True return False elif self.autolist and (self.field_type.check_value(value) or self.field_type.can_use_value(value)): return True else:
return False @property def autolist(self): """ autolist getter: autolist flag allows to convert a simple item on a list with one item. """ return self._autolist @autolist.setter def autolist(self, value): """ autolist setter: autolist flag allows to convert a simple item on a list with one item. """
self._autolist = value
[docs]class HashMapField(InnerFieldTypeMixin, ModelField): """ It allows to create a field which contains a hash map. **Automatic cast from:** * :class:`dict`. * :class:`BaseModel`. """ def __init__(self, model_class=None, **kwargs): if model_class is None: from dirty_models.models import HashMapModel model_class = HashMapModel super(HashMapField, self).__init__(model_class=model_class, **kwargs)
[docs] def convert_value(self, value):
return self._model_class(data=value, field_type=self.field_type)
[docs]class BlobField(BaseField): """ It allows any type of data. """
pass
[docs]class MultiTypeField(BaseField): """ It allows to define multiple type for a field. So, it is possible to define a field as a integer and as a model field, for example. """ def __init__(self, field_types=None, **kwargs): self._field_types = [] field_types = field_types or [] for field_type in field_types: if isinstance(field_type, tuple): field_type = field_type[0](**field_type[1]) self._field_types.append(field_type if field_type else BaseField()) super(MultiTypeField, self).__init__(**kwargs) def get_field_docstring(self): if len(self._field_types): return 'Multiple type values are allowed:\n\n{0}'.format( "\n\n".join(["* {0}".format(field.get_field_docstring()) for field in self._field_types])) def export_definition(self): result = super(MultiTypeField, self).export_definition() result['field_types'] = [(field_type.__class__, field_type.export_definition()) for field_type in self._field_types] return result
[docs] def convert_value(self, value): for ft in self._field_types: if ft.can_use_value(value):
return ft.convert_value(value)
[docs] def check_value(self, value): for ft in self._field_types: if ft.check_value(value): return True
return False
[docs] def can_use_value(self, value): for ft in self._field_types: if ft.can_use_value(value): return True
return False def get_field_type_by_value(self, value): for ft in self._field_types: if ft.check_value(value): return ft for ft in self._field_types: if ft.can_use_value(value): return ft raise TypeError("Value `{0}` can not be used on field `{1}`".format(value, self.name)) @property def field_types(self):
return self._field_types.copy()
[docs]class EnumField(BaseField): """ It allows to create a field which contains a member of an enumeration. **Automatic cast from:** * Any value of enumeration. * Any member name of enumeration. """ def __init__(self, enum_class, *args, **kwargs): """ :param enum_class: Enumeration class :type enum_class: enum.Enum """ self.enum_class = enum_class super(EnumField, self).__init__(*args, **kwargs) def export_definition(self): result = super(EnumField, self).export_definition() result['enum_class'] = self.enum_class return result def get_field_docstring(self): dcstr = super(EnumField, self).get_field_docstring() if self.enum_class: dcstr += ' (:class:`{0}`)'.format('.'.join([self.enum_class.__module__, self.enum_class.__name__])) return dcstr
[docs] def convert_value(self, value): try: return self.enum_class(value) except ValueError:
return getattr(self.enum_class, value)
[docs] def check_value(self, value):
return isinstance(value, self.enum_class)
[docs] def can_use_value(self, value): try: self.enum_class(value) return True except ValueError: pass
return value in self.enum_class.__members__.keys() class BytesField(BaseField): """ It allows to use a bytes as value in a field. **Automatic cast from:** * :class:`str` * :class:`int` * :class:`bytearray` * :class:`list` of :class:`int` in range(0, 256) * :class:`~enum.Enum` if value of enum can be cast. """ @convert_enum def convert_value(self, value): if isinstance(value, str): return value.encode() elif isinstance(value, (list, ListModel, bytearray, int)): if isinstance(value, int): value = bytes([value, ]) elif isinstance(value, ListModel): value = value.export_data() try: return bytes(value) except TypeError: pass return None def check_value(self, value): return isinstance(value, bytes) @can_use_enum def can_use_value(self, value): return isinstance(value, (int, str, list, ListModel, bytearray))