import re import typing from datetime import timedelta from decimal import Decimal, InvalidOperation import babelfish from knowit.core import Configurable, Property from knowit.utils import round_decimal T = typing.TypeVar('T') class Basic(Property[T]): """Basic property to handle int, Decimal and other basic types.""" def __init__(self, *args: str, data_type: typing.Type, processor: typing.Optional[typing.Callable[[T], T]] = None, allow_fallback: bool = False, **kwargs): """Init method.""" super().__init__(*args, **kwargs) self.data_type = data_type self.processor = processor or (lambda x: x) self.allow_fallback = allow_fallback def handle(self, value, context: typing.MutableMapping): """Handle value.""" if isinstance(value, self.data_type): return self.processor(value) try: return self.processor(self.data_type(value)) except ValueError: if not self.allow_fallback: self.report(value, context) class Duration(Property[timedelta]): """Duration property.""" duration_re = re.compile(r'(?P\d{1,2}):' r'(?P\d{1,2}):' r'(?P\d{1,2})(?:\.' r'(?P\d{3})' r'(?P\d{3})?\d*)?') def __init__(self, *args: str, resolution: typing.Union[int, Decimal] = 1, **kwargs): """Initialize a Duration.""" super().__init__(*args, **kwargs) self.resolution = resolution def handle(self, value, context: typing.MutableMapping): """Return duration as timedelta.""" if isinstance(value, timedelta): return value elif isinstance(value, int): return timedelta(milliseconds=int(value * self.resolution)) try: return timedelta( milliseconds=int(Decimal(value) * self.resolution)) except (ValueError, InvalidOperation): pass match = self.duration_re.match(value) if not match: self.report(value, context) return None params = { key: int(value) for key, value in match.groupdict().items() if value } return timedelta(**params) class Language(Property[babelfish.Language]): """Language property.""" def handle(self, value, context: typing.MutableMapping): """Handle languages.""" try: if len(value) == 3: return babelfish.Language.fromalpha3b(value) return babelfish.Language.fromietf(value) except (babelfish.Error, ValueError): pass try: return babelfish.Language.fromname(value) except babelfish.Error: pass self.report(value, context) return babelfish.Language('und') class Quantity(Property): """Quantity is a property with unit.""" def __init__(self, *args: str, unit, data_type=int, **kwargs): """Init method.""" super().__init__(*args, **kwargs) self.unit = unit self.data_type = data_type def handle(self, value, context): """Handle value with unit.""" if not isinstance(value, self.data_type): try: value = self.data_type(value) except ValueError: self.report(value, context) return if isinstance(value, Decimal): value = round_decimal(value, min_digits=1, max_digits=3) return value if context.get('no_units') else value * self.unit class YesNo(Configurable[str]): """Yes or No handler.""" yes_values = ('yes', 'true', '1') def __init__(self, *args: str, yes=True, no=False, hide_value=None, config: typing.Optional[ typing.Mapping[str, typing.Mapping]] = None, config_key: typing.Optional[str] = None, **kwargs): """Init method.""" super().__init__(config or {}, config_key=config_key, *args, **kwargs) self.yes = yes self.no = no self.hide_value = hide_value def handle(self, value, context): """Handle boolean values.""" result = self.yes if str(value).lower() in self.yes_values else self.no if result == self.hide_value: return None return super().handle(result, context) if self.mapping else result