Source code for flask_jsonrpc.funcutils

# Copyright (c) 2024-2025, Cenobit Technologies, Inc. http://cenobit.es/
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
# * Neither the name of the Cenobit Technologies nor the names of
#    its contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import annotations

from enum import Enum
import typing as t
from decimal import Decimal
import inspect
from collections import defaultdict
import dataclasses
from collections.abc import Set, Mapping, Sequence, Collection, MutableSet, MutableMapping, MutableSequence

import typing_inspect
from typing_extensions import Buffer

from pydantic import ValidationError
from pydantic.main import BaseModel, create_model

from flask_jsonrpc.types import types as jsonrpc_types
from flask_jsonrpc.helpers import from_python_type


[docs] def loads(param_type: t.Any, param_value: t.Any) -> t.Any: # noqa: ANN401, C901 """Deserialize a JSON-RPC parameter value to the specified type. Args: param_type (typing.Any): The type to deserialize to. param_value (typing.Any): The parameter value to deserialize. Returns: typing.Any: The deserialized parameter value. Raises: TypeError: If the parameter value cannot be deserialized to the specified type. TypeError: If an unsupported union type is provided. Examples: >>> loads(int, 42) 42 >>> loads(list[int], [1, 2, 3]) [1, 2, 3] >>> from enum import Enum >>> class Color(Enum): ... RED = 'red' ... GREEN = 'green' >>> loads(Color, 'red') <Color.RED: 'red'> >>> from pydantic import BaseModel >>> class User(BaseModel): ... id: int ... name: str >>> loads(User, {'id': 1, 'name': 'Alice'}) User(id=1, name='Alice') """ if param_value is None: return param_value if param_type is t.Any: return param_value origin_type = t.get_origin(param_type) if origin_type is t.Annotated: annotated_origin_type = getattr(param_type, '__origin__', type(None)) return loads(annotated_origin_type, param_value) # XXX: The only type of union that is supported is: typing.Union[T, None] or typing.Optional[T] if typing_inspect.is_union_type(param_type) or typing_inspect.is_optional_type(param_type): obj_types = t.get_args(param_type) if len(obj_types) == 2: actual_type, check_type = obj_types if actual_type is type(None): actual_type, check_type = check_type, actual_type if check_type is type(None): return loads(actual_type, param_value) raise TypeError( 'the only type of union that is supported is: typing.Union[T, None] or typing.Optional[T]' ) from None jsonrpc_type = from_python_type(param_type, default=None) if jsonrpc_type is None: if inspect.isclass(param_type): if issubclass(param_type, Enum): return param_type(param_value) if issubclass(param_type, BaseModel): base_model = t.cast(type[BaseModel], param_type) # type: ignore model = create_model(base_model.__name__, __base__=base_model) try: return model.model_validate(param_value) except ValidationError as e: raise TypeError(str(e)) from e # XXX: typing.NamedTuple if issubclass(param_type, tuple) and not typing_inspect.is_tuple_type(param_type): return param_type(**param_value) if dataclasses.is_dataclass(param_type): return param_type(**param_value) return param_type(**param_value) return param_value if ( jsonrpc_types.Number.name == jsonrpc_type.name and inspect.isclass(param_type) and issubclass(param_type, Decimal) ): return param_type(str(param_value)) if jsonrpc_types.Object.name == jsonrpc_type.name: loaded_dict = {} key_type, value_type = t.get_args(param_type) for key, value in param_value.items(): loaded_key = loads(key_type, key) loaded_value = loads(value_type, value) loaded_dict[loaded_key] = loaded_value dict_param_type_origin: t.Any = t.get_origin(param_type) if dict_param_type_origin is defaultdict: return defaultdict(None, loaded_dict) if any(dict_param_type_origin is tp for tp in (Mapping, MutableMapping)): return loaded_dict return dict_param_type_origin(loaded_dict) if jsonrpc_types.Array.name == jsonrpc_type.name: loaded_list = [] item_type = t.get_args(param_type)[0] for item in param_value: loaded_list.append(loads(item_type, item)) list_param_type_origin: t.Any = t.get_origin(param_type) if any(list_param_type_origin is tp for tp in (Sequence, MutableSequence, Collection)): return loaded_list if any(list_param_type_origin is tp for tp in (Set, MutableSet)): return set(loaded_list) return list_param_type_origin(loaded_list) if typing_inspect.is_literal_type(param_type): return param_value if typing_inspect.is_final_type(param_type): return param_value if issubclass(param_type, bytes | bytearray): return param_type(param_value.encode('utf-8')) if issubclass(param_type, Buffer): # pyright: ignore[reportGeneralTypeIssues] return memoryview(param_value.encode('utf-8')) return param_value
[docs] def bindfy(view_func: t.Callable[..., t.Any], params: dict[str, t.Any]) -> dict[str, t.Any]: # noqa: ANN401 """Bind JSON-RPC parameters to a view function's parameters with type deserialization. Args: view_func (typing.Callable[..., typing.Any]): The view function to bind parameters to. params (dict[str, typing.Any]): The JSON-RPC parameters to bind. Returns: dict[str, typing.Any]: The bound parameters with deserialized values. See Also: :func:`flask_jsonrpc.funcutils.loads` """ binded_params = {} view_func_params = getattr(view_func, 'jsonrpc_method_params', {}) view_func_default_params = getattr(view_func, 'jsonrpc_method_default_params', {}) for param_name, param_type in view_func_params.items(): param_value = params.get(param_name) param_default_value = view_func_default_params.get(param_name, None) binded_params[param_name] = loads(param_type, param_value if param_value is not None else param_default_value) return binded_params