Source code for flask_jsonrpc.helpers

# Copyright (c) 2020-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

import typing as t
from operator import getitem
import itertools
from dataclasses import field, asdict, dataclass

# Added in version 3.11.
from typing_extensions import Self

from flask_jsonrpc.types.types import Types, Object

if t.TYPE_CHECKING:
    from flask_jsonrpc.types.types import JSONRPCNewType


[docs] @dataclass class Node: """A node in a tree structure. Args: name (str | None): Name of the node. items (list[dict[str, typing.Any]]): List of items in the node. children (list[Node]): List of child nodes. Attributes: name (str | None): Name of the node. items (list[dict[str, typing.Any]]): List of items in the node. children (list[Node]): List of child nodes. Examples: >>> root = Node(name='root') >>> child1 = Node(name='child1') >>> child2 = Node(name='child2') >>> root.add_child(child1) >>> root.add_child(child2) >>> child1.insert_item({'name': 'item1'}) >>> child2.insert_item({'name': 'item2'}) >>> assert root.to_dict() == { ... 'name': 'root', ... 'items': [], ... 'children': [ ... {'name': 'child1', 'items': [{'name': 'item1'}], 'children': []}, ... {'name': 'child2', 'items': [{'name': 'item2'}], 'children': []}, ... ], ... } >>> root.find_child('child1').to_dict() {'name': 'child1', 'items': [{'name': 'item1'}], 'children': []} >>> >>> root.find_child('child3') is None True >>> root.clean() >>> root.sort() >>> assert root.to_dict() == { ... 'name': 'root', ... 'items': [], ... 'children': [ ... {'name': 'child1', 'items': [{'name': 'item1'}], 'children': []}, ... {'name': 'child2', 'items': [{'name': 'item2'}], 'children': []}, ... ], ... } """ name: str | None items: list[dict[str, t.Any]] = field(default_factory=list) children: list[Node] = field(default_factory=list)
[docs] def find_child(self: Self, name: str) -> Node | None: """Find a child node by name. Args: name (str): Name of the child node to find. Returns: Node | None: The child node if found, otherwise `None`. """ for child in self.children: if child.name == name: return child return None
[docs] def add_child(self: Self, node: Node) -> None: """Add a child node. Args: node (Node): Child node to add. """ self.children.append(node)
[docs] def insert_item(self: Self, val: dict[str, t.Any]) -> None: """Insert an item into the node. Args: val (dict[str, typing.Any]): Item to insert. """ self.items.append(val)
[docs] def clean(self: Self) -> None: """Clean the node by removing empty children.""" for child in self.children: child.clean() self.children = [child for child in self.children if child.items or child.children]
[docs] def sort(self: Self) -> None: """Sort the node's children and items by name.""" def sort_by_name(n: Node) -> str: return n.name or '' self.children.sort(key=sort_by_name) self.items.sort(key=lambda i: i.get('name', '')) for child in self.children: child.sort()
[docs] def to_dict(self: Self) -> dict[str, t.Any]: """Convert the node to a dictionary. Returns: dict[str, typing.Any]: Dictionary representation of the node. """ return asdict(self)
[docs] def urn(name: str, *args: t.Any) -> str: # noqa: ANN401 """Return the URN name. Args: name (str): Name. *args (typing.Any): Additional name parts. Returns: str: URN name. Examples: >>> urn('python') 'urn:python' >>> urn('python.flask') 'urn:python:flask' >>> urn('python', 'Flask', 'JsonRPC') 'urn:python:flask:jsonrpc' >>> urn('python', '/api/browse') 'urn:python:api:browse' >>> urn(None) Traceback (most recent call last): ... ValueError: name is required >>> urn('') Traceback (most recent call last): ... ValueError: name is required """ if not name: raise ValueError('name is required') from None splitted_params = [arg.replace('.', '/').replace(':', '/').split('/') for arg in [name] + list(args)] values = ['urn'] + [st for st in list(itertools.chain(*splitted_params)) if st != ''] return ':'.join(values).lower()
[docs] def from_python_type(tp: t.Any, default: JSONRPCNewType | None = Object) -> JSONRPCNewType | None: # noqa: ANN401 """Convert Python type to JSONRPCNewType. Args: tp (typing.Any): Python type. default (flask_jsonrpc.types.types.JSONRPCNewType | None, optional): Default type if no match is found. Defaults to Object. Returns: flask_jsonrpc.types.types.JSONRPCNewType | None: Corresponding JSONRPCNewType or `default`. Examples: >>> str(from_python_type(str)) 'String' >>> str(from_python_type(int)) 'Number' >>> str(from_python_type(dict)) 'Object' >>> str(from_python_type(list)) 'Array' >>> str(from_python_type(bool)) 'Boolean' >>> str(from_python_type(None)) 'Null' >>> str(from_python_type(t.NoReturn)) 'Null' """ for typ in Types: if typ.check_type(tp): return typ return default
[docs] def get(obj: t.Any, path: str, default: t.Any = None) -> t.Any: # noqa: ANN401 """Get the value at any depth of a nested object based on the path described by `path`. If path doesn't exist, `default` is returned. Args: obj (dict): Object to process. path (str): List or `.` delimited string of path describing path. Keyword Arguments: default (typing.Any): Default value to return if path doesn't exist. Defaults to ``None``. Returns: typing.Any: Value of `obj` at path. Examples: >>> get(None, 'a') >>> get(None, 'a', 'default') 'default' >>> get('a', 'a.b.c', 'default') 'default' >>> get({'a': 1}, 'a') 1 >>> get({'a': 1}, 'b') >>> get({'a': 1}, 'b', 'default') 'default' >>> get({'a': {'b': {'c': 1}}}, 'a.b.c') 1 >>> get({}, 'a.b.c') >>> get([], 'a.b.c') >>> get([], 'a.b.c', None) """ if obj is None: return default if not isinstance(obj, dict): return default if path in obj: return getitem(obj, path) obj_val = obj keys = path.split('.') try: for key in keys: obj_val = getitem(obj_val, key) except (TypeError, KeyError): return default return obj_val