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
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
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
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
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
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
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
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
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
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
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
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
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)
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)
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, )
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()
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()
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()
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
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()
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()