Source code for core.converters

""" Contains custom converters. """

import isodate
import morepath

from datetime import date, datetime
from onegov.core.framework import Framework
from onegov.core.orm.abstract import MoveDirection
from onegov.core.utils import is_uuid
from onegov.core.custom import custom_json as json
from time import mktime, strptime
from uuid import UUID


from typing import get_args, get_origin, overload, Any, Literal, TYPE_CHECKING
if TYPE_CHECKING:
    from collections.abc import Mapping
    from typing import LiteralString


@overload
[docs] def extended_date_decode(s: Literal['']) -> None: ... # type:ignore
@overload def extended_date_decode(s: str) -> date: ... def extended_date_decode(s: str) -> date | None: """ Decodes a date string HTML5 (RFC3339) compliant.""" if not s: return None try: return date.fromtimestamp(mktime(strptime(s, '%Y-%m-%d'))) except OverflowError as exception: raise ValueError() from exception
[docs] def extended_date_encode(d: date | None) -> str: """ Encodes a date HTML5 (RFC3339) compliant. """ if not d: return '' return d.strftime('%Y-%m-%d')
[docs] extended_date_converter = morepath.Converter( decode=extended_date_decode, encode=extended_date_encode )
@overload # type:ignore[overload-overlap]
[docs] def json_decode(s: Literal['']) -> None: ...
@overload def json_decode(s: str) -> dict[str, Any]: ... # NOTE: Technically this is incorrect, but we assume, we only ever # decode a JSON object, and not JSON in general def json_decode(s: str) -> dict[str, Any] | None: """ Decodes a json string to a dict. """ if not s: return None return json.loads(s)
[docs] def json_encode(d: 'Mapping[str, Any] | None') -> str: """ Encodes a dictionary to json. """ if not d: return '{}' return json.dumps(d)
[docs] json_converter = morepath.Converter( decode=json_decode, encode=json_encode )
[docs] def uuid_decode(s: str) -> UUID | None: """ Turns a uuid string into a UUID instance. """ return is_uuid(s) and UUID(s) or None
[docs] def uuid_encode(uuid: UUID | str | None) -> str: """ Turns a UUID instance into a uuid string. """ if not uuid: return '' if isinstance(uuid, str): return uuid return uuid.hex
[docs] uuid_converter = morepath.Converter( decode=uuid_decode, encode=uuid_encode )
@Framework.converter(type=UUID)
[docs] def get_default_uuid_converter() -> 'morepath.Converter[UUID]': return uuid_converter
@overload
[docs] def bool_decode(s: Literal['0', '']) -> Literal[False]: ...
@overload def bool_decode(s: Literal['1']) -> Literal[True]: ... @overload def bool_decode(s: str) -> bool: ... def bool_decode(s: str) -> bool: """ Decodes a boolean. """ return not (s == '0' or s == '') @overload
[docs] def bool_encode(d: Literal[False] | None) -> Literal['0']: ...
@overload def bool_encode(d: Literal[True]) -> Literal['1']: ... @overload def bool_encode(d: bool | None) -> Literal['0', '1']: ... def bool_encode(d: bool | None) -> Literal['0', '1']: """ Encodes a boolean. """ return d and '1' or '0'
[docs] bool_converter: 'morepath.Converter[bool]' = morepath.Converter( decode=bool_decode, encode=bool_encode )
@Framework.converter(type=bool)
[docs] def get_default_bool_converter() -> 'morepath.Converter[bool]': return bool_converter
@overload
[docs] def datetime_decode(s: Literal['']) -> None: ... # type:ignore
@overload def datetime_decode(s: str) -> datetime: ... def datetime_decode(s: str) -> datetime | None: """ Decodes a datetime. """ return None if not s else isodate.parse_datetime(s)
[docs] def datetime_encode(d: datetime | None) -> str: """ Encodes a datetime. """ return isodate.datetime_isoformat(d) if d else ''
[docs] datetime_converter = morepath.Converter( decode=datetime_decode, encode=datetime_encode )
@Framework.converter(type=datetime)
[docs] def get_default_datetime_converter() -> 'morepath.Converter[datetime]': return datetime_converter
[docs] def integer_range_decode(s: str) -> tuple[int, int] | None: if not s: return None s, _, e = s.partition('-') return int(s), int(e)
[docs] def integer_range_encode(t: tuple[int, int] | None) -> str: return t and f'{t[0]}-{t[1]}' or ''
[docs] integer_range_converter = morepath.Converter( decode=integer_range_decode, encode=integer_range_encode )
[docs] def move_direction_decode(s: str) -> MoveDirection | None: try: return MoveDirection[s] except KeyError: return None
# we are slightly more lax and allow arbitrary str values when encoding # so we can provide e.g. a template string that gets replaced later on
[docs] def move_direction_encode(d: str | MoveDirection | None) -> str: if d is None: return '' elif isinstance(d, str): return d return d.name
[docs] move_direction_converter = morepath.Converter( decode=move_direction_decode, encode=move_direction_encode )
@Framework.converter(type=MoveDirection)
[docs] def get_default_move_direction_converter( ) -> 'morepath.Converter[MoveDirection]': return move_direction_converter
if TYPE_CHECKING:
[docs] LiteralConverterBase = morepath.Converter[LiteralString]
else: LiteralConverterBase = morepath.Converter
[docs] class LiteralConverter(LiteralConverterBase): """ This is a ``Converter`` counter-part to ``typing.Literal``. """ # TODO: This should use TypeForm eventually @overload def __init__(self, literal_type: Any, /) -> None: ... @overload def __init__(self, *literals: 'LiteralString') -> None: ... def __init__(self, *literals: Any) -> None: if len(literals) == 1 and get_origin(literals[0]) is Literal: literals = get_args(literals[0]) if not all(isinstance(v, str) for v in literals): # TODO: Consider supporting float/int literals via their # respective converters in the future raise ValueError('We only support string literals for simplicity')
[docs] self.allowed_values: set[str] = set(literals)
[docs] def single_decode(self, string: str) -> str | None: return string if string in self.allowed_values else None
[docs] def single_encode(self, value: str | None) -> str: # NOTE: If we ever support non-string literals we may need to actually # encode them here. return str(value) if value is not None else ''