예제 #1
0
파일: codec.py 프로젝트: fondat/fondat-core
def _typeddict(codec_type, python_type):

    python_type, _ = split_annotated(python_type)

    if not is_typeddict(python_type):
        return

    if codec_type is JSON:

        if c := _building.get((codec_type, python_type)):
            return c  # return the (incomplete) outer one still being built

        hints = get_type_hints(python_type, include_extras=True)

        for key in hints:
            if type(key) is not str:
                raise TypeError("codec only supports TypedDict with str keys")

        @affix_type_hints(localns=locals())
        class _TypedDict_JSON(JSON[python_type]):

            json_type = dict[str, Any]  # will be replaced below

            def _process(self, value, method):
                result = {}
                for key in hints:
                    codec = get_codec(JSON, hints[key])
                    try:
                        with CodecError.path_on_error(key):
                            result[key] = getattr(codec, method)(value[key])
                    except KeyError:
                        continue
                return result

            def encode(self, value: python_type) -> Any:
                if not isinstance(value, dict):
                    raise EncodeError
                return self._process(value, "encode")

            def decode(self, value: Any) -> python_type:
                return self._process(value, "decode")

        result = _TypedDict_JSON()
        _building[(codec_type, python_type)] = result

        try:
            json_type = TypedDict(
                "_TypedDict",
                {key: get_codec(JSON, hints[key]).json_type
                 for key in hints},
                total=python_type.__total__,
            )
            json_type.__required_keys__ = python_type.__required_keys__
            json_type.__optional_keys__ = python_type.__optional_keys__
            result.json_type = result.__class__.json_type = json_type

        finally:
            del _building[(codec_type, python_type)]

        return result
예제 #2
0
def get_codec(python_type: Any) -> SQLiteCodec:
    """Return a codec compatible with the specified Python type."""

    python_type, _ = split_annotated(python_type)

    for provider in codec_providers:
        if (codec := provider(python_type)) is not None:
            return codec
예제 #3
0
파일: codec.py 프로젝트: fondat/fondat-core
def _float(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if is_subclass(python_type, float):
        if codec_type is Binary:
            return _float_binarycodec
        if codec_type is String:
            return _float_stringcodec
        if codec_type is JSON:
            return _float_jsoncodec
예제 #4
0
파일: codec.py 프로젝트: fondat/fondat-core
def _int(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if is_subclass(python_type, int) and not is_subclass(python_type, bool):
        if codec_type is Binary:
            return _int_binarycodec
        if codec_type is String:
            return _int_stringcodec
        if codec_type is JSON:
            return _int_jsoncodec
예제 #5
0
파일: codec.py 프로젝트: fondat/fondat-core
def _NoneType(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if python_type is NoneType:
        if codec_type is Binary:
            return _nonetype_binarycodec
        if codec_type is String:
            return _nonetype_stringcodec
        if codec_type is JSON:
            return _nonetype_jsoncodec
예제 #6
0
파일: codec.py 프로젝트: fondat/fondat-core
def _bool(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if is_subclass(python_type, bool):
        if codec_type is Binary:
            return _bool_binarycodec
        if codec_type is String:
            return _bool_stringcodec
        if codec_type is JSON:
            return _bool_jsoncodec
예제 #7
0
파일: codec.py 프로젝트: fondat/fondat-core
def _Decimal(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if is_subclass(python_type, Decimal):
        if codec_type is Binary:
            return _decimal_binary
        if codec_type is String:
            return _decimal_string
        if codec_type is JSON:
            return _decimal_json
예제 #8
0
파일: codec.py 프로젝트: fondat/fondat-core
def _datetime(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if is_subclass(python_type, datetime):
        if codec_type is Binary:
            return _datetime_binarycodec
        if codec_type is String:
            return _datetime_stringcodec
        if codec_type is JSON:
            return _datetime_jsoncodec
예제 #9
0
파일: codec.py 프로젝트: fondat/fondat-core
def _uuid(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if is_subclass(python_type, UUID):
        if codec_type is Binary:
            return _uuid_binarycodec
        if codec_type is String:
            return _uuid_stringcodec
        if codec_type is JSON:
            return _uuid_jsoncodec
예제 #10
0
파일: codec.py 프로젝트: fondat/fondat-core
def _bytes(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if is_subclass(python_type, (bytes, bytearray)):
        if codec_type is Binary:
            return _bytes_binarycodec
        if codec_type is String:
            return _bytes_stringcodec
        if codec_type is JSON:
            return _bytes_jsoncodec
예제 #11
0
파일: codec.py 프로젝트: fondat/fondat-core
def _any(codec_type, python_type):
    python_type, _ = split_annotated(python_type)
    if python_type is Any:
        if codec_type is Binary:
            return _any_binarycodec
        if codec_type is String:
            return _any_stringcodec
        if codec_type is JSON:
            return _any_jsoncodec
예제 #12
0
파일: codec.py 프로젝트: fondat/fondat-core
def _get_codec(codec_type, python_type, annotations):

    _, annotations = split_annotated(python_type)

    for annotation in annotations:
        if isinstance(annotation, codec_type):
            return annotation

    for provider in providers:
        if (codec := provider(codec_type, python_type)) is not None:
            return codec
예제 #13
0
파일: data.py 프로젝트: fondat/fondat-core
def redact_passwords(hint: Any, value: Any, redaction: str = "__REDACTED__"):
    """
    Redact password fields in dataclass or TypedDict value.
    """
    if is_dataclass(value):
        getter, setter = functools.partial(getattr, value), functools.partial(
            setattr, value)
    elif isinstance(value, Mapping):
        getter, setter = value.get, value.__setitem__
    else:
        raise TypeError("type must be dataclass or TypedDict")
    value_type, _ = split_annotated(strip_optional(hint))
    for field_name, field_hint in value_type.__annotations__.items():
        field_type, field_annotations = split_annotated(
            strip_optional(field_hint))
        field_value = getter(field_name)
        if hasattr(field_type, "__annotations__") and (
                is_dataclass(field_value) or isinstance(field_value, Mapping)):
            redact_passwords(field_hint, field_value)
        elif (field_value is not None and is_subclass(field_type, str)
              and Password in field_annotations):
            setter(field_name, redaction)
예제 #14
0
def validate(value: Any, type_hint: Any) -> NoneType:
    """Validate a value."""

    python_type, annotations = split_annotated(type_hint)
    origin = typing.get_origin(python_type)
    args = typing.get_args(python_type)

    # validate using specified validator annotations
    for annotation in annotations:
        if isinstance(annotation, Validator):
            annotation.validate(value)

    # aggregate type validation
    if python_type is Any:
        return
    elif origin is Union:
        return _validate_union(value, args)
    elif origin is Literal:
        return _validate_literal(value, args)

    # TypedDict
    if is_subclass(python_type, dict) and hasattr(python_type,
                                                  "__annotations__"):
        origin = dict

    # basic type validation
    if origin and not is_instance(value, origin):
        raise ValidationError(
            f"expecting {origin.__name__}; received {type(value)}")
    elif not origin and not is_instance(value, python_type):
        raise ValidationError(
            f"expecting {python_type}; received {type(value)}")
    elif python_type is int and is_instance(value,
                                            bool):  # bool is subclass of int
        raise ValidationError("expecting int; received bool")
    elif is_subclass(origin, Iterable) and is_instance(
            value, (str, bytes, bytearray)):
        raise ValidationError(f"expecting Iterable; received {type(value)}")

    # structured type validation
    if is_subclass(python_type, dict) and hasattr(python_type,
                                                  "__annotations__"):
        return _validate_typeddict(value, python_type)
    elif is_subclass(origin, Mapping):
        return _validate_mapping(value, python_type, args)
    elif is_subclass(origin, tuple):
        return _validate_tuple(value, python_type, args)
    elif is_subclass(origin, Iterable):
        return _validate_iterable(value, python_type, args)
    elif dataclasses.is_dataclass(python_type):
        return _validate_dataclass(value, python_type)
예제 #15
0
파일: data.py 프로젝트: fondat/fondat-core
def derive_typeddict(
    type_name: str,
    source: Any,
    *,
    include: set[str] = None,
    exclude: set[str] = None,
    total: bool = True,
) -> type:
    """
    Generate a derived TypedDict from a source TypedDict or dataclass.

    Parameters:
    • type_name: the name of the new TypedDict type
    • source: TypedDict or dataclass to derive from
    • include: the names of keys to include  [all]
    • exclude: the names of keys to exclude  [none]
    • total: must all keys be present in the TypedDict
    """

    source, _ = split_annotated(source)

    if include is None:
        include = source.__annotations__.keys()

    if exclude is None:
        exclude = set()

    return TypedDict(
        type_name,
        {
            key: type
            for key, type in source.__annotations__.items()
            if key in include and key not in exclude
        },
        total=total,
    )
예제 #16
0
파일: codec.py 프로젝트: fondat/fondat-core
def _tuple(codec_type, python_type):

    pytype, _ = split_annotated(python_type)

    if pytype is tuple:
        origin = tuple
        args = (Any, ...)

    else:
        origin = get_origin(pytype)
        args = get_args(pytype)

    if origin is not tuple:
        return

    if len(args) != 2 and Ellipsis in args or args[0] is Ellipsis:
        raise TypeError("unexpected ellipsis")

    varg = args[0] if len(args) == 2 and args[1] is Ellipsis else None
    args = () if varg else args

    if codec_type is JSON:

        codecs = [get_codec(JSON, arg) for arg in args]
        vcodec = get_codec(JSON, varg) if varg else None

        @affix_type_hints(localns=locals())
        class _Tuple_JSON(JSON[python_type]):

            json_type = list[Any]

            def encode(self, value: python_type) -> list[Any]:
                if not isinstance(value, tuple) or (args and
                                                    len(value) != len(args)):
                    raise EncodeError
                # TODO: path
                return ([vcodec.encode(item)
                         for item in value] if vcodec else [
                             codecs[n].encode(value[n])
                             for n in range(len(codecs))
                         ])

            def decode(self, value: list[Any]) -> python_type:
                if not isinstance(value, list) or (args and
                                                   len(value) != len(args)):
                    raise DecodeError
                # TODO: path
                return tuple(
                    [vcodec.decode(item) for item in value] if vcodec else
                    [codecs[n].decode(value[n]) for n in range(len(codecs))])

        return _Tuple_JSON()

    if codec_type is String:

        codecs = [get_codec(String, arg) for arg in args]
        vcodec = get_codec(String, varg) if varg else None

        @affix_type_hints(localns=locals())
        class _Tuple_String(String[python_type]):
            def encode(self, value: python_type) -> str:
                if not isinstance(value, tuple) or (args and
                                                    len(value) != len(args)):
                    raise EncodeError
                # TODO: path
                return _csv_encode(
                    [vcodec.encode(item) for item in value] if vcodec else
                    [codecs[n].encode(value[n]) for n in range(len(codecs))])

            def decode(self, value: str) -> python_type:
                decoded = _csv_decode(value)
                if args and len(decoded) != len(args):
                    raise DecodeError
                # TODO: path
                return tuple(
                    [vcodec.decode(item) for item in decoded] if vcodec else
                    [codecs[n].decode(decoded[n]) for n in range(len(codecs))])

        return _Tuple_String()

    if codec_type is Binary:

        json_codec = get_codec(JSON, python_type)

        @affix_type_hints(localns=locals())
        class _Tuple_Binary(Binary[python_type]):

            content_type = "application/json"

            def encode(self, value: python_type) -> bytes:
                return json.dumps(json_codec.encode(value)).encode()

            def decode(self, value: Union[bytes, bytearray]) -> python_type:
                return json_codec.decode(_s2j(_b2s(value)))

        return _Tuple_Binary()
예제 #17
0
파일: codec.py 프로젝트: fondat/fondat-core
def _mapping(codec_type, python_type):

    python_type, _ = split_annotated(python_type)

    if is_subclass(python_type, Mapping):
        origin = Mapping
        args = [Any, Any]

    else:
        origin = get_origin(python_type)
        if not is_subclass(origin, Mapping) or getattr(
                python_type, "__annotations__", None):
            return  # not a Mapping
        args = get_args(python_type)
        if len(args) != 2:
            raise TypeError("expecting Mapping[KT, VT]")

    if codec_type is JSON:
        key_codec = get_codec(String, args[0])
        value_codec = get_codec(JSON, args[1])
        _json_type = dict[str, value_codec.json_type]

        @affix_type_hints(localns=locals())
        class _Mapping_JSON(JSON[python_type]):
            json_type = _json_type

            def encode(self, value: python_type) -> _json_type:
                if not isinstance(value, Mapping):
                    raise EncodeError
                result = {}
                for k, v in value.items():
                    key = key_codec.encode(k)
                    with CodecError.path_on_error(key):
                        result[key] = value_codec.encode(v)
                return result

            def decode(self, value: _json_type) -> python_type:
                if not isinstance(value, Mapping):
                    raise DecodeError
                result = {}
                for k, v in value.items():
                    key = key_codec.decode(k)
                    with CodecError.path_on_error(key):
                        result[key] = value_codec.decode(v)
                return result

        return _Mapping_JSON()

    if codec_type is String:
        json_codec = get_codec(JSON, python_type)

        @affix_type_hints(localns=locals())
        class _Mapping_String(String[python_type]):
            def encode(self, value: python_type) -> str:
                if not isinstance(value, Mapping):
                    raise EncodeError
                return json.dumps(json_codec.encode(value))

            def decode(self, value: str) -> python_type:
                return json_codec.decode(_s2j(value))

        return _Mapping_String()

    if codec_type is Binary:

        string_codec = get_codec(String, python_type)

        @affix_type_hints(localns=locals())
        class _Mapping_Binary(Binary[python_type]):

            content_type = "application/json"

            def encode(self, value: python_type) -> bytes:
                if not isinstance(value, Mapping):
                    raise EncodeError
                return string_codec.encode(value).encode()

            def decode(self, value: Union[bytes, bytearray]) -> python_type:
                return string_codec.decode(_b2s(value))

        return _Mapping_Binary()
예제 #18
0
파일: codec.py 프로젝트: fondat/fondat-core
def _iterable(codec_type, python_type):

    decode_type = list if get_origin(python_type) is Iterable else python_type

    python_type, _ = split_annotated(python_type)

    if is_subclass(python_type,
                   Iterable) and not is_subclass(python_type,
                                                 (str, bytes, bytearray)):
        origin = python_type
        args = (Any, )

    else:
        origin = get_origin(python_type)
        if not is_subclass(origin, Iterable) or is_subclass(origin, Mapping):
            return
        args = get_args(python_type)

    if len(args) != 1:
        raise TypeError("expecting Iterable[T]")

    item_type = args[0]
    is_set = is_subclass(origin, set)

    if codec_type is JSON:

        item_codec = get_codec(JSON, item_type)
        _json_type = list[item_codec.json_type]

        @affix_type_hints(localns=locals())
        class _Iterable_JSON(JSON[python_type]):

            json_type = _json_type

            def encode(self, value: python_type) -> _json_type:
                if not isinstance(value, Iterable) or isinstance(value, str):
                    raise EncodeError
                if is_set:
                    value = sorted(value)
                # TODO: path
                return [item_codec.encode(item) for item in value]

            def decode(self, value: _json_type) -> python_type:
                if not isinstance(value, list):
                    raise DecodeError
                # TODO: path
                return decode_type((item_codec.decode(item) for item in value))

        return _Iterable_JSON()

    if codec_type is String:

        item_codec = get_codec(String, item_type)

        @affix_type_hints(localns=locals())
        class _Iterable_String(String[python_type]):
            def encode(self, value: python_type) -> str:
                if not isinstance(value, Iterable) or isinstance(value, str):
                    raise EncodeError
                if is_set:
                    value = sorted(value)
                return _csv_encode((item_codec.encode(item) for item in value))

            def decode(self, value: str) -> python_type:
                # TODO: path
                return decode_type(
                    (item_codec.decode(item) for item in _csv_decode(value)))

        return _Iterable_String()

    if codec_type is Binary:

        json_codec = get_codec(JSON, python_type)

        @affix_type_hints(localns=locals())
        class _Iterable_Binary(Binary[python_type]):

            content_type = "application/json"

            def encode(self, value: python_type) -> bytes:
                if not isinstance(value, Iterable) or isinstance(value, str):
                    raise EncodeError
                return json.dumps(json_codec.encode(value)).encode()

            def decode(self, value: Union[bytes, bytearray]) -> python_type:
                return json_codec.decode(_s2j(_b2s(value)))

        return _Iterable_Binary()
예제 #19
0
파일: codec.py 프로젝트: fondat/fondat-core
def dataclass_codec(codec_type, python_type):

    dc_type, _ = split_annotated(python_type)

    if not dataclasses.is_dataclass(dc_type):
        return

    fields = dataclasses.fields(dc_type)

    if codec_type is JSON:

        if c := _building.get((codec_type, python_type)):
            return c  # return the (incomplete) outer one still being built

        hints = get_type_hints(dc_type, include_extras=True)

        @affix_type_hints(localns=locals())
        class _Dataclass_JSON(JSON[python_type]):

            json_type = dict[str, Any]  # will be replaced below

            def encode(self, value: python_type) -> Any:
                if not isinstance(value, dc_type):
                    raise EncodeError
                result = {}
                for field in fields:
                    v = getattr(value, field.name, None)
                    if v is not None:
                        with CodecError.path_on_error(field.name):
                            result[_dc_kw.get(field.name,
                                              field.name)] = get_codec(
                                                  JSON, field.type).encode(v)
                return result

            def decode(self, value: Any) -> python_type:
                if not isinstance(value, dict):
                    raise DecodeError
                kwargs = {}
                for field in fields:
                    codec = get_codec(JSON, field.type)
                    try:
                        with CodecError.path_on_error(field.name):
                            kwargs[field.name] = codec.decode(value[_dc_kw.get(
                                field.name, field.name)])
                    except KeyError:
                        if (is_optional(field.type)
                                and field.default is dataclasses.MISSING and
                                field.default_factory is dataclasses.MISSING):
                            kwargs[field.name] = None
                try:
                    return python_type(**kwargs)
                except Exception as e:
                    raise DecodeError from e

        result = _Dataclass_JSON()
        _building[(codec_type, python_type)] = result

        try:
            json_type = TypedDict(
                "_TypedDict",
                {key: get_codec(JSON, hints[key]).json_type
                 for key in hints},
                total=False,
            )

            # workaround for https://bugs.python.org/issue42059
            json_type.__required_keys__ = frozenset()
            json_type.__optional_keys__ = frozenset(hints)

            result.json_type = result.__class__.json_type = json_type

        finally:
            del _building[(codec_type, python_type)]

        return result
예제 #20
0
파일: codec.py 프로젝트: fondat/fondat-core
def _union(codec_type, python_type):

    python_type, _ = split_annotated(python_type)
    origin = get_origin(python_type)

    if origin is not Union:
        return

    types = get_args(python_type)
    codecs = [get_codec(codec_type, type) for type in types]

    def _encode(value):
        for codec in codecs:
            try:
                return codec.encode(value)
            except EncodeError:
                continue
        raise EncodeError(f"{python_type} as {codec_type} for value: {value}")

    def _decode(value):
        for codec in codecs:
            try:
                return codec.decode(value)
            except DecodeError:
                continue
        raise DecodeError(f"{python_type} as {codec_type} for value: {value}")

    if codec_type is String:

        @affix_type_hints(localns=locals())
        class _Union_String(String[python_type]):
            def encode(self, value: python_type) -> str:
                if value is None and NoneType in types:
                    return ""
                return _encode(value)

            def decode(self, value: str) -> python_type:
                return _decode(value)

        return _Union_String()

    if codec_type is Binary:

        @affix_type_hints(localns=locals())
        class _Union_Binary(Binary[python_type]):

            content_type = "application/octet-stream"

            def encode(self, value: python_type) -> bytes:
                if value is None and NoneType in types:
                    return b""
                return _encode(value)

            def decode(self, value: Union[bytes, bytearray]) -> python_type:
                if not isinstance(value, (bytes, bytearray)):
                    raise DecodeError
                return _decode(value)

        return _Union_Binary()

    if codec_type is JSON:

        _json_type = Union[tuple(codec.json_type for codec in codecs)]

        @affix_type_hints(localns=locals())
        class _Union_JSON(JSON[python_type]):

            json_type = _json_type

            def encode(self, value: python_type) -> _json_type:
                if value is None and NoneType in types:
                    return None
                return _encode(value)

            def decode(self, value: _json_type) -> python_type:
                return _decode(value)

        return _Union_JSON()
예제 #21
0
파일: codec.py 프로젝트: fondat/fondat-core
def _literal(codec_type, python_type):

    python_type, _ = split_annotated(python_type)
    origin = get_origin(python_type)

    if origin is not Literal:
        return

    literals = {(type(l), l) for l in get_args(python_type)}

    def decode(codecs, value):
        for codec in codecs:
            try:
                v = getattr(codec, "decode")(value)
                if (type(v), v) in literals:
                    return v
            except:
                continue
        raise DecodeError(
            f"expecting one of: {get_args(python_type)}; received: {value}")

    if codec_type is String:

        codecs = tuple(get_codec(String, literal[0]) for literal in literals)

        @affix_type_hints(localns=locals())
        class _Literal_String(String[python_type]):
            def encode(self, value: python_type) -> str:
                return get_codec(String, type(value)).encode(value)

            def decode(self, value: str) -> python_type:
                return decode(codecs, value)

        return _Literal_String()

    if codec_type is Binary:

        codecs = tuple(get_codec(Binary, literal[0]) for literal in literals)

        @affix_type_hints(localns=locals())
        class _Literal_Binary(Binary[python_type]):

            content_type = "application/octet-stream"

            def encode(self, value: python_type) -> bytes:
                return get_codec(Binary, type(value)).encode(value)

            def decode(self, value: Union[bytes, bytearray]) -> python_type:
                if not isinstance(value, (bytes, bytearray)):
                    raise DecodeError
                return decode(codecs, value)

        return _Literal_Binary()

    if codec_type is JSON:

        codecs = tuple(get_codec(JSON, literal[0]) for literal in literals)
        _json_type = Union[tuple(codec.json_type for codec in codecs)]

        @affix_type_hints(localns=locals())
        class _Literal_JSON(JSON[python_type]):

            json_type = _json_type

            def encode(self, value: python_type) -> _json_type:
                return get_codec(JSON, type(value)).encode(value)

            def decode(self, value: _json_type) -> python_type:
                return decode(codecs, value)

        return _Literal_JSON()