added 3rd party packages, elog, bigtree
This commit is contained in:
0
python310/packages/bigtree/tree/__init__.py
Normal file
0
python310/packages/bigtree/tree/__init__.py
Normal file
1327
python310/packages/bigtree/tree/construct.py
Normal file
1327
python310/packages/bigtree/tree/construct.py
Normal file
File diff suppressed because it is too large
Load Diff
1660
python310/packages/bigtree/tree/export.py
Normal file
1660
python310/packages/bigtree/tree/export.py
Normal file
File diff suppressed because it is too large
Load Diff
415
python310/packages/bigtree/tree/helper.py
Normal file
415
python310/packages/bigtree/tree/helper.py
Normal file
@@ -0,0 +1,415 @@
|
||||
from collections import deque
|
||||
from typing import Any, Deque, Dict, List, Set, Type, TypeVar, Union
|
||||
|
||||
from bigtree.node.basenode import BaseNode
|
||||
from bigtree.node.binarynode import BinaryNode
|
||||
from bigtree.node.node import Node
|
||||
from bigtree.tree.construct import add_dict_to_tree_by_path, dataframe_to_tree
|
||||
from bigtree.tree.export import tree_to_dataframe
|
||||
from bigtree.tree.search import find_path
|
||||
from bigtree.utils.exceptions import NotFoundError
|
||||
from bigtree.utils.iterators import levelordergroup_iter
|
||||
|
||||
__all__ = ["clone_tree", "get_subtree", "prune_tree", "get_tree_diff"]
|
||||
BaseNodeT = TypeVar("BaseNodeT", bound=BaseNode)
|
||||
BinaryNodeT = TypeVar("BinaryNodeT", bound=BinaryNode)
|
||||
NodeT = TypeVar("NodeT", bound=Node)
|
||||
|
||||
|
||||
def clone_tree(tree: BaseNode, node_type: Type[BaseNodeT]) -> BaseNodeT:
|
||||
"""Clone tree to another ``Node`` type.
|
||||
If the same type is needed, simply do a tree.copy().
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import BaseNode, Node, clone_tree
|
||||
>>> root = BaseNode(name="a")
|
||||
>>> b = BaseNode(name="b", parent=root)
|
||||
>>> clone_tree(root, Node)
|
||||
Node(/a, )
|
||||
|
||||
Args:
|
||||
tree (BaseNode): tree to be cloned, must inherit from BaseNode
|
||||
node_type (Type[BaseNode]): type of cloned tree
|
||||
|
||||
Returns:
|
||||
(BaseNode)
|
||||
"""
|
||||
if not isinstance(tree, BaseNode):
|
||||
raise TypeError("Tree should be of type `BaseNode`, or inherit from `BaseNode`")
|
||||
|
||||
# Start from root
|
||||
root_info = dict(tree.root.describe(exclude_prefix="_"))
|
||||
root_node = node_type(**root_info)
|
||||
|
||||
def _recursive_add_child(
|
||||
_new_parent_node: BaseNodeT, _parent_node: BaseNode
|
||||
) -> None:
|
||||
"""Recursively clone current node
|
||||
|
||||
Args:
|
||||
_new_parent_node (BaseNode): cloned parent node
|
||||
_parent_node (BaseNode): parent node to be cloned
|
||||
"""
|
||||
for _child in _parent_node.children:
|
||||
if _child:
|
||||
child_info = dict(_child.describe(exclude_prefix="_"))
|
||||
child_node = node_type(**child_info)
|
||||
child_node.parent = _new_parent_node
|
||||
_recursive_add_child(child_node, _child)
|
||||
|
||||
_recursive_add_child(root_node, tree.root)
|
||||
return root_node
|
||||
|
||||
|
||||
def get_subtree(
|
||||
tree: NodeT,
|
||||
node_name_or_path: str = "",
|
||||
max_depth: int = 0,
|
||||
) -> NodeT:
|
||||
"""Get subtree based on node name or node path, and/or maximum depth of tree.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, get_subtree
|
||||
>>> root = Node("a")
|
||||
>>> b = Node("b", parent=root)
|
||||
>>> c = Node("c", parent=b)
|
||||
>>> d = Node("d", parent=b)
|
||||
>>> e = Node("e", parent=root)
|
||||
>>> root.show()
|
||||
a
|
||||
├── b
|
||||
│ ├── c
|
||||
│ └── d
|
||||
└── e
|
||||
|
||||
Get subtree
|
||||
|
||||
>>> root_subtree = get_subtree(root, "b")
|
||||
>>> root_subtree.show()
|
||||
b
|
||||
├── c
|
||||
└── d
|
||||
|
||||
Args:
|
||||
tree (Node): existing tree
|
||||
node_name_or_path (str): node name or path to get subtree, defaults to None
|
||||
max_depth (int): maximum depth of subtree, based on `depth` attribute, defaults to None
|
||||
|
||||
Returns:
|
||||
(Node)
|
||||
"""
|
||||
tree = tree.copy()
|
||||
|
||||
if node_name_or_path:
|
||||
tree = find_path(tree, node_name_or_path)
|
||||
if not tree:
|
||||
raise ValueError(f"Node name or path {node_name_or_path} not found")
|
||||
|
||||
if not tree.is_root:
|
||||
tree.parent = None
|
||||
|
||||
if max_depth:
|
||||
tree = prune_tree(tree, max_depth=max_depth)
|
||||
return tree
|
||||
|
||||
|
||||
def prune_tree(
|
||||
tree: Union[BinaryNodeT, NodeT],
|
||||
prune_path: Union[List[str], str] = "",
|
||||
exact: bool = False,
|
||||
sep: str = "/",
|
||||
max_depth: int = 0,
|
||||
) -> Union[BinaryNodeT, NodeT]:
|
||||
"""Prune tree by path or depth, returns the root of a *copy* of the original tree.
|
||||
|
||||
For pruning by `prune_path`,
|
||||
|
||||
- All siblings along the prune path will be removed.
|
||||
- If ``exact=True``, all descendants of prune path will be removed.
|
||||
- Prune path can be string (only one path) or a list of strings (multiple paths).
|
||||
- Prune path name should be unique, can be full path, partial path (trailing part of path), or node name.
|
||||
|
||||
For pruning by `max_depth`,
|
||||
|
||||
- All nodes that are beyond `max_depth` will be removed.
|
||||
|
||||
Path should contain ``Node`` name, separated by `sep`.
|
||||
|
||||
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, prune_tree
|
||||
>>> root = Node("a")
|
||||
>>> b = Node("b", parent=root)
|
||||
>>> c = Node("c", parent=b)
|
||||
>>> d = Node("d", parent=b)
|
||||
>>> e = Node("e", parent=root)
|
||||
>>> root.show()
|
||||
a
|
||||
├── b
|
||||
│ ├── c
|
||||
│ └── d
|
||||
└── e
|
||||
|
||||
Prune (default is keep descendants)
|
||||
|
||||
>>> root_pruned = prune_tree(root, "a/b")
|
||||
>>> root_pruned.show()
|
||||
a
|
||||
└── b
|
||||
├── c
|
||||
└── d
|
||||
|
||||
Prune exact path
|
||||
|
||||
>>> root_pruned = prune_tree(root, "a/b", exact=True)
|
||||
>>> root_pruned.show()
|
||||
a
|
||||
└── b
|
||||
|
||||
Prune multiple paths
|
||||
|
||||
>>> root_pruned = prune_tree(root, ["a/b/d", "a/e"])
|
||||
>>> root_pruned.show()
|
||||
a
|
||||
├── b
|
||||
│ └── d
|
||||
└── e
|
||||
|
||||
Prune by depth
|
||||
|
||||
>>> root_pruned = prune_tree(root, max_depth=2)
|
||||
>>> root_pruned.show()
|
||||
a
|
||||
├── b
|
||||
└── e
|
||||
|
||||
Args:
|
||||
tree (Union[BinaryNode, Node]): existing tree
|
||||
prune_path (List[str] | str): prune path(s), all siblings along the prune path(s) will be removed
|
||||
exact (bool): prune path(s) to be exactly the path, defaults to False (descendants of the path are retained)
|
||||
sep (str): path separator of `prune_path`
|
||||
max_depth (int): maximum depth of pruned tree, based on `depth` attribute, defaults to None
|
||||
|
||||
Returns:
|
||||
(Union[BinaryNode, Node])
|
||||
"""
|
||||
if isinstance(prune_path, str):
|
||||
prune_path = [prune_path] if prune_path else []
|
||||
|
||||
if not len(prune_path) and not max_depth:
|
||||
raise ValueError("Please specify either `prune_path` or `max_depth` or both.")
|
||||
|
||||
tree_copy = tree.copy()
|
||||
|
||||
# Prune by path (prune bottom-up)
|
||||
if len(prune_path):
|
||||
ancestors_to_prune: Set[Union[BinaryNodeT, NodeT]] = set()
|
||||
nodes_to_prune: Set[Union[BinaryNodeT, NodeT]] = set()
|
||||
for path in prune_path:
|
||||
path = path.replace(sep, tree.sep)
|
||||
child = find_path(tree_copy, path)
|
||||
if not child:
|
||||
raise NotFoundError(
|
||||
f"Cannot find any node matching path_name ending with {path}"
|
||||
)
|
||||
nodes_to_prune.add(child)
|
||||
ancestors_to_prune.update(list(child.ancestors))
|
||||
|
||||
if exact:
|
||||
ancestors_to_prune.update(nodes_to_prune)
|
||||
|
||||
for node in ancestors_to_prune:
|
||||
for child in node.children:
|
||||
if (
|
||||
child
|
||||
and child not in ancestors_to_prune
|
||||
and child not in nodes_to_prune
|
||||
):
|
||||
child.parent = None
|
||||
|
||||
# Prune by depth (prune top-down)
|
||||
if max_depth:
|
||||
for depth, level_nodes in enumerate(levelordergroup_iter(tree_copy), 1):
|
||||
if depth == max_depth:
|
||||
for level_node in level_nodes:
|
||||
del level_node.children
|
||||
return tree_copy
|
||||
|
||||
|
||||
def get_tree_diff(
|
||||
tree: Node, other_tree: Node, only_diff: bool = True, attr_list: List[str] = []
|
||||
) -> Node:
|
||||
"""Get difference of `tree` to `other_tree`, changes are relative to `tree`.
|
||||
|
||||
Compares the difference in tree structure (default), but can also compare tree attributes using `attr_list`.
|
||||
Function can return only the differences (default), or all original tree nodes and differences.
|
||||
|
||||
Comparing tree structure:
|
||||
|
||||
- (+) and (-) will be added to node name relative to `tree`.
|
||||
- For example: (+) refers to nodes that are in `other_tree` but not `tree`.
|
||||
- For example: (-) refers to nodes that are in `tree` but not `other_tree`.
|
||||
|
||||
Examples:
|
||||
>>> # Create original tree
|
||||
>>> from bigtree import Node, get_tree_diff, list_to_tree
|
||||
>>> root = list_to_tree(["Downloads/Pictures/photo1.jpg", "Downloads/file1.doc", "Downloads/photo2.jpg"])
|
||||
>>> root.show()
|
||||
Downloads
|
||||
├── Pictures
|
||||
│ └── photo1.jpg
|
||||
├── file1.doc
|
||||
└── photo2.jpg
|
||||
|
||||
>>> # Create other tree
|
||||
>>> root_other = list_to_tree(["Downloads/Pictures/photo1.jpg", "Downloads/Pictures/photo2.jpg", "Downloads/file1.doc"])
|
||||
>>> root_other.show()
|
||||
Downloads
|
||||
├── Pictures
|
||||
│ ├── photo1.jpg
|
||||
│ └── photo2.jpg
|
||||
└── file1.doc
|
||||
|
||||
>>> # Get tree differences
|
||||
>>> tree_diff = get_tree_diff(root, root_other)
|
||||
>>> tree_diff.show()
|
||||
Downloads
|
||||
├── photo2.jpg (-)
|
||||
└── Pictures
|
||||
└── photo2.jpg (+)
|
||||
|
||||
>>> tree_diff = get_tree_diff(root, root_other, only_diff=False)
|
||||
>>> tree_diff.show()
|
||||
Downloads
|
||||
├── Pictures
|
||||
│ ├── photo1.jpg
|
||||
│ └── photo2.jpg (+)
|
||||
├── file1.doc
|
||||
└── photo2.jpg (-)
|
||||
|
||||
Comparing tree attributes
|
||||
|
||||
- (~) will be added to node name if there are differences in tree attributes defined in `attr_list`.
|
||||
- The node's attributes will be a list of [value in `tree`, value in `other_tree`]
|
||||
|
||||
>>> # Create original tree
|
||||
>>> root = Node("Downloads")
|
||||
>>> picture_folder = Node("Pictures", parent=root)
|
||||
>>> photo2 = Node("photo1.jpg", tags="photo1", parent=picture_folder)
|
||||
>>> file1 = Node("file1.doc", tags="file1", parent=root)
|
||||
>>> root.show(attr_list=["tags"])
|
||||
Downloads
|
||||
├── Pictures
|
||||
│ └── photo1.jpg [tags=photo1]
|
||||
└── file1.doc [tags=file1]
|
||||
|
||||
>>> # Create other tree
|
||||
>>> root_other = Node("Downloads")
|
||||
>>> picture_folder = Node("Pictures", parent=root_other)
|
||||
>>> photo1 = Node("photo1.jpg", tags="photo1-edited", parent=picture_folder)
|
||||
>>> photo2 = Node("photo2.jpg", tags="photo2-new", parent=picture_folder)
|
||||
>>> file1 = Node("file1.doc", tags="file1", parent=root_other)
|
||||
>>> root_other.show(attr_list=["tags"])
|
||||
Downloads
|
||||
├── Pictures
|
||||
│ ├── photo1.jpg [tags=photo1-edited]
|
||||
│ └── photo2.jpg [tags=photo2-new]
|
||||
└── file1.doc [tags=file1]
|
||||
|
||||
>>> # Get tree differences
|
||||
>>> tree_diff = get_tree_diff(root, root_other, attr_list=["tags"])
|
||||
>>> tree_diff.show(attr_list=["tags"])
|
||||
Downloads
|
||||
└── Pictures
|
||||
├── photo1.jpg (~) [tags=('photo1', 'photo1-edited')]
|
||||
└── photo2.jpg (+)
|
||||
|
||||
Args:
|
||||
tree (Node): tree to be compared against
|
||||
other_tree (Node): tree to be compared with
|
||||
only_diff (bool): indicator to show all nodes or only nodes that are different (+/-), defaults to True
|
||||
attr_list (List[str]): tree attributes to check for difference, defaults to empty list
|
||||
|
||||
Returns:
|
||||
(Node)
|
||||
"""
|
||||
other_tree.sep = tree.sep
|
||||
name_col = "name"
|
||||
path_col = "PATH"
|
||||
indicator_col = "Exists"
|
||||
|
||||
data, data_other = (
|
||||
tree_to_dataframe(
|
||||
_tree,
|
||||
name_col=name_col,
|
||||
path_col=path_col,
|
||||
attr_dict={k: k for k in attr_list},
|
||||
)
|
||||
for _tree in (tree, other_tree)
|
||||
)
|
||||
|
||||
# Check tree structure difference
|
||||
data_both = data[[path_col, name_col] + attr_list].merge(
|
||||
data_other[[path_col, name_col] + attr_list],
|
||||
how="outer",
|
||||
on=[path_col, name_col],
|
||||
indicator=indicator_col,
|
||||
)
|
||||
|
||||
# Handle tree structure difference
|
||||
nodes_removed = list(data_both[data_both[indicator_col] == "left_only"][path_col])[
|
||||
::-1
|
||||
]
|
||||
nodes_added = list(data_both[data_both[indicator_col] == "right_only"][path_col])[
|
||||
::-1
|
||||
]
|
||||
for node_removed in nodes_removed:
|
||||
data_both[path_col] = data_both[path_col].str.replace(
|
||||
node_removed, f"{node_removed} (-)", regex=True
|
||||
)
|
||||
for node_added in nodes_added:
|
||||
data_both[path_col] = data_both[path_col].str.replace(
|
||||
node_added, f"{node_added} (+)", regex=True
|
||||
)
|
||||
|
||||
# Check tree attribute difference
|
||||
path_changes_list_of_dict: List[Dict[str, Dict[str, Any]]] = []
|
||||
path_changes_deque: Deque[str] = deque([])
|
||||
for attr_change in attr_list:
|
||||
condition_diff = (
|
||||
(
|
||||
~data_both[f"{attr_change}_x"].isnull()
|
||||
| ~data_both[f"{attr_change}_y"].isnull()
|
||||
)
|
||||
& (data_both[f"{attr_change}_x"] != data_both[f"{attr_change}_y"])
|
||||
& (data_both[indicator_col] == "both")
|
||||
)
|
||||
data_diff = data_both[condition_diff]
|
||||
if len(data_diff):
|
||||
tuple_diff = zip(
|
||||
data_diff[f"{attr_change}_x"], data_diff[f"{attr_change}_y"]
|
||||
)
|
||||
dict_attr_diff = [{attr_change: v} for v in tuple_diff]
|
||||
dict_path_diff = dict(list(zip(data_diff[path_col], dict_attr_diff)))
|
||||
path_changes_list_of_dict.append(dict_path_diff)
|
||||
path_changes_deque.extend(list(data_diff[path_col]))
|
||||
|
||||
if only_diff:
|
||||
data_both = data_both[
|
||||
(data_both[indicator_col] != "both")
|
||||
| (data_both[path_col].isin(path_changes_deque))
|
||||
]
|
||||
data_both = data_both[[path_col]]
|
||||
if len(data_both):
|
||||
tree_diff = dataframe_to_tree(data_both, node_type=tree.__class__)
|
||||
# Handle tree attribute difference
|
||||
if len(path_changes_deque):
|
||||
path_changes_list = sorted(path_changes_deque, reverse=True)
|
||||
name_changes_list = [
|
||||
{k: {"name": f"{k.split(tree.sep)[-1]} (~)"} for k in path_changes_list}
|
||||
]
|
||||
path_changes_list_of_dict.extend(name_changes_list)
|
||||
for attr_change_dict in path_changes_list_of_dict:
|
||||
tree_diff = add_dict_to_tree_by_path(tree_diff, attr_change_dict)
|
||||
return tree_diff
|
||||
1356
python310/packages/bigtree/tree/modify.py
Normal file
1356
python310/packages/bigtree/tree/modify.py
Normal file
File diff suppressed because it is too large
Load Diff
479
python310/packages/bigtree/tree/search.py
Normal file
479
python310/packages/bigtree/tree/search.py
Normal file
@@ -0,0 +1,479 @@
|
||||
from typing import Any, Callable, Iterable, List, Tuple, TypeVar, Union
|
||||
|
||||
from bigtree.node.basenode import BaseNode
|
||||
from bigtree.node.dagnode import DAGNode
|
||||
from bigtree.node.node import Node
|
||||
from bigtree.utils.exceptions import SearchError
|
||||
from bigtree.utils.iterators import preorder_iter
|
||||
|
||||
__all__ = [
|
||||
"findall",
|
||||
"find",
|
||||
"find_name",
|
||||
"find_names",
|
||||
"find_relative_path",
|
||||
"find_full_path",
|
||||
"find_path",
|
||||
"find_paths",
|
||||
"find_attr",
|
||||
"find_attrs",
|
||||
"find_children",
|
||||
"find_child",
|
||||
"find_child_by_name",
|
||||
]
|
||||
|
||||
|
||||
T = TypeVar("T", bound=BaseNode)
|
||||
NodeT = TypeVar("NodeT", bound=Node)
|
||||
DAGNodeT = TypeVar("DAGNodeT", bound=DAGNode)
|
||||
|
||||
|
||||
def findall(
|
||||
tree: T,
|
||||
condition: Callable[[T], bool],
|
||||
max_depth: int = 0,
|
||||
min_count: int = 0,
|
||||
max_count: int = 0,
|
||||
) -> Tuple[T, ...]:
|
||||
"""
|
||||
Search tree for nodes matching condition (callable function).
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, findall
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> findall(root, lambda node: node.age > 62)
|
||||
(Node(/a, age=90), Node(/a/b, age=65))
|
||||
|
||||
Args:
|
||||
tree (BaseNode): tree to search
|
||||
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
|
||||
max_depth (int): maximum depth to search for, based on the `depth` attribute, defaults to None
|
||||
min_count (int): checks for minimum number of occurrences,
|
||||
raise SearchError if the number of results do not meet min_count, defaults to None
|
||||
max_count (int): checks for maximum number of occurrences,
|
||||
raise SearchError if the number of results do not meet min_count, defaults to None
|
||||
|
||||
Returns:
|
||||
(Tuple[BaseNode, ...])
|
||||
"""
|
||||
result = tuple(preorder_iter(tree, filter_condition=condition, max_depth=max_depth))
|
||||
if min_count and len(result) < min_count:
|
||||
raise SearchError(
|
||||
f"Expected more than {min_count} element(s), found {len(result)} elements\n{result}"
|
||||
)
|
||||
if max_count and len(result) > max_count:
|
||||
raise SearchError(
|
||||
f"Expected less than {max_count} element(s), found {len(result)} elements\n{result}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def find(tree: T, condition: Callable[[T], bool], max_depth: int = 0) -> T:
|
||||
"""
|
||||
Search tree for *single node* matching condition (callable function).
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find(root, lambda node: node.age == 65)
|
||||
Node(/a/b, age=65)
|
||||
>>> find(root, lambda node: node.age > 5)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
bigtree.utils.exceptions.SearchError: Expected less than 1 element(s), found 4 elements
|
||||
(Node(/a, age=90), Node(/a/b, age=65), Node(/a/c, age=60), Node(/a/c/d, age=40))
|
||||
|
||||
Args:
|
||||
tree (BaseNode): tree to search
|
||||
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
|
||||
max_depth (int): maximum depth to search for, based on the `depth` attribute, defaults to None
|
||||
|
||||
Returns:
|
||||
(BaseNode)
|
||||
"""
|
||||
result = findall(tree, condition, max_depth, max_count=1)
|
||||
if result:
|
||||
return result[0]
|
||||
|
||||
|
||||
def find_name(tree: NodeT, name: str, max_depth: int = 0) -> NodeT:
|
||||
"""
|
||||
Search tree for single node matching name attribute.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_name
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_name(root, "c")
|
||||
Node(/a/c, age=60)
|
||||
|
||||
Args:
|
||||
tree (Node): tree to search
|
||||
name (str): value to match for name attribute
|
||||
max_depth (int): maximum depth to search for, based on the `depth` attribute, defaults to None
|
||||
|
||||
Returns:
|
||||
(Node)
|
||||
"""
|
||||
return find(tree, lambda node: node.node_name == name, max_depth)
|
||||
|
||||
|
||||
def find_names(tree: NodeT, name: str, max_depth: int = 0) -> Iterable[NodeT]:
|
||||
"""
|
||||
Search tree for multiple node(s) matching name attribute.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_names
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("b", age=40, parent=c)
|
||||
>>> find_names(root, "c")
|
||||
(Node(/a/c, age=60),)
|
||||
>>> find_names(root, "b")
|
||||
(Node(/a/b, age=65), Node(/a/c/b, age=40))
|
||||
|
||||
Args:
|
||||
tree (Node): tree to search
|
||||
name (str): value to match for name attribute
|
||||
max_depth (int): maximum depth to search for, based on the `depth` attribute, defaults to None
|
||||
|
||||
Returns:
|
||||
(Iterable[Node])
|
||||
"""
|
||||
return findall(tree, lambda node: node.node_name == name, max_depth)
|
||||
|
||||
|
||||
def find_relative_path(tree: NodeT, path_name: str) -> Iterable[NodeT]:
|
||||
r"""
|
||||
Search tree for single node matching relative path attribute.
|
||||
|
||||
- Supports unix folder expression for relative path, i.e., '../../node_name'
|
||||
- Supports wildcards, i.e., '\*/node_name'
|
||||
- If path name starts with leading separator symbol, it will start at root node.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_relative_path
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_relative_path(d, "..")
|
||||
(Node(/a/c, age=60),)
|
||||
>>> find_relative_path(d, "../../b")
|
||||
(Node(/a/b, age=65),)
|
||||
>>> find_relative_path(d, "../../*")
|
||||
(Node(/a/b, age=65), Node(/a/c, age=60))
|
||||
|
||||
Args:
|
||||
tree (Node): tree to search
|
||||
path_name (str): value to match (relative path) of path_name attribute
|
||||
|
||||
Returns:
|
||||
(Iterable[Node])
|
||||
"""
|
||||
sep = tree.sep
|
||||
if path_name.startswith(sep):
|
||||
resolved_node = find_full_path(tree, path_name)
|
||||
return (resolved_node,)
|
||||
path_name = path_name.rstrip(sep).lstrip(sep)
|
||||
path_list = path_name.split(sep)
|
||||
wildcard_indicator = "*" in path_name
|
||||
resolved_nodes: List[NodeT] = []
|
||||
|
||||
def resolve(node: NodeT, path_idx: int) -> None:
|
||||
"""Resolve node based on path name
|
||||
|
||||
Args:
|
||||
node (Node): current node
|
||||
path_idx (int): current index in path_list
|
||||
"""
|
||||
if path_idx == len(path_list):
|
||||
resolved_nodes.append(node)
|
||||
else:
|
||||
path_component = path_list[path_idx]
|
||||
if path_component == ".":
|
||||
resolve(node, path_idx + 1)
|
||||
elif path_component == "..":
|
||||
if node.is_root:
|
||||
raise SearchError("Invalid path name. Path goes beyond root node.")
|
||||
resolve(node.parent, path_idx + 1)
|
||||
elif path_component == "*":
|
||||
for child in node.children:
|
||||
resolve(child, path_idx + 1)
|
||||
else:
|
||||
node = find_child_by_name(node, path_component)
|
||||
if not node:
|
||||
if not wildcard_indicator:
|
||||
raise SearchError(
|
||||
f"Invalid path name. Node {path_component} cannot be found."
|
||||
)
|
||||
else:
|
||||
resolve(node, path_idx + 1)
|
||||
|
||||
resolve(tree, 0)
|
||||
|
||||
return tuple(resolved_nodes)
|
||||
|
||||
|
||||
def find_full_path(tree: NodeT, path_name: str) -> NodeT:
|
||||
"""
|
||||
Search tree for single node matching path attribute.
|
||||
|
||||
- Path name can be with or without leading tree path separator symbol.
|
||||
- Path name must be full path, works similar to `find_path` but faster.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_full_path
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_full_path(root, "/a/c/d")
|
||||
Node(/a/c/d, age=40)
|
||||
|
||||
Args:
|
||||
tree (Node): tree to search
|
||||
path_name (str): value to match (full path) of path_name attribute
|
||||
|
||||
Returns:
|
||||
(Node)
|
||||
"""
|
||||
sep = tree.sep
|
||||
path_list = path_name.rstrip(sep).lstrip(sep).split(sep)
|
||||
if path_list[0] != tree.root.node_name:
|
||||
raise ValueError(
|
||||
f"Path {path_name} does not match the root node name {tree.root.node_name}"
|
||||
)
|
||||
parent_node = tree.root
|
||||
child_node = parent_node
|
||||
for child_name in path_list[1:]:
|
||||
child_node = find_child_by_name(parent_node, child_name)
|
||||
if not child_node:
|
||||
break
|
||||
parent_node = child_node
|
||||
return child_node
|
||||
|
||||
|
||||
def find_path(tree: NodeT, path_name: str) -> NodeT:
|
||||
"""
|
||||
Search tree for single node matching path attribute.
|
||||
|
||||
- Path name can be with or without leading tree path separator symbol.
|
||||
- Path name can be full path or partial path (trailing part of path) or node name.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_path
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_path(root, "c")
|
||||
Node(/a/c, age=60)
|
||||
>>> find_path(root, "/c")
|
||||
Node(/a/c, age=60)
|
||||
|
||||
Args:
|
||||
tree (Node): tree to search
|
||||
path_name (str): value to match (full path) or trailing part (partial path) of path_name attribute
|
||||
|
||||
Returns:
|
||||
(Node)
|
||||
"""
|
||||
path_name = path_name.rstrip(tree.sep)
|
||||
return find(tree, lambda node: node.path_name.endswith(path_name))
|
||||
|
||||
|
||||
def find_paths(tree: NodeT, path_name: str) -> Tuple[NodeT, ...]:
|
||||
"""
|
||||
Search tree for multiple nodes matching path attribute.
|
||||
|
||||
- Path name can be with or without leading tree path separator symbol.
|
||||
- Path name can be partial path (trailing part of path) or node name.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_paths
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("c", age=40, parent=c)
|
||||
>>> find_paths(root, "/a/c")
|
||||
(Node(/a/c, age=60),)
|
||||
>>> find_paths(root, "/c")
|
||||
(Node(/a/c, age=60), Node(/a/c/c, age=40))
|
||||
|
||||
Args:
|
||||
tree (Node): tree to search
|
||||
path_name (str): value to match (full path) or trailing part (partial path) of path_name attribute
|
||||
|
||||
Returns:
|
||||
(Tuple[Node, ...])
|
||||
"""
|
||||
path_name = path_name.rstrip(tree.sep)
|
||||
return findall(tree, lambda node: node.path_name.endswith(path_name))
|
||||
|
||||
|
||||
def find_attr(
|
||||
tree: BaseNode, attr_name: str, attr_value: Any, max_depth: int = 0
|
||||
) -> BaseNode:
|
||||
"""
|
||||
Search tree for single node matching custom attribute.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_attr
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_attr(root, "age", 65)
|
||||
Node(/a/b, age=65)
|
||||
|
||||
Args:
|
||||
tree (BaseNode): tree to search
|
||||
attr_name (str): attribute name to perform matching
|
||||
attr_value (Any): value to match for attr_name attribute
|
||||
max_depth (int): maximum depth to search for, based on the `depth` attribute, defaults to None
|
||||
|
||||
Returns:
|
||||
(BaseNode)
|
||||
"""
|
||||
return find(
|
||||
tree,
|
||||
lambda node: bool(node.get_attr(attr_name) == attr_value),
|
||||
max_depth,
|
||||
)
|
||||
|
||||
|
||||
def find_attrs(
|
||||
tree: BaseNode, attr_name: str, attr_value: Any, max_depth: int = 0
|
||||
) -> Tuple[BaseNode, ...]:
|
||||
"""
|
||||
Search tree for node(s) matching custom attribute.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_attrs
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=65, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_attrs(root, "age", 65)
|
||||
(Node(/a/b, age=65), Node(/a/c, age=65))
|
||||
|
||||
Args:
|
||||
tree (BaseNode): tree to search
|
||||
attr_name (str): attribute name to perform matching
|
||||
attr_value (Any): value to match for attr_name attribute
|
||||
max_depth (int): maximum depth to search for, based on the `depth` attribute, defaults to None
|
||||
|
||||
Returns:
|
||||
(Tuple[BaseNode, ...])
|
||||
"""
|
||||
return findall(
|
||||
tree,
|
||||
lambda node: bool(node.get_attr(attr_name) == attr_value),
|
||||
max_depth,
|
||||
)
|
||||
|
||||
|
||||
def find_children(
|
||||
tree: Union[T, DAGNodeT],
|
||||
condition: Callable[[Union[T, DAGNodeT]], bool],
|
||||
min_count: int = 0,
|
||||
max_count: int = 0,
|
||||
) -> Tuple[Union[T, DAGNodeT], ...]:
|
||||
"""
|
||||
Search children for nodes matching condition (callable function).
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_children
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_children(root, lambda node: node.age > 30)
|
||||
(Node(/a/b, age=65), Node(/a/c, age=60))
|
||||
|
||||
Args:
|
||||
tree (BaseNode/DAGNode): tree to search for its children
|
||||
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
|
||||
min_count (int): checks for minimum number of occurrences,
|
||||
raise SearchError if the number of results do not meet min_count, defaults to None
|
||||
max_count (int): checks for maximum number of occurrences,
|
||||
raise SearchError if the number of results do not meet min_count, defaults to None
|
||||
|
||||
Returns:
|
||||
(BaseNode/DAGNode)
|
||||
"""
|
||||
result = tuple([node for node in tree.children if node and condition(node)])
|
||||
if min_count and len(result) < min_count:
|
||||
raise SearchError(
|
||||
f"Expected more than {min_count} element(s), found {len(result)} elements\n{result}"
|
||||
)
|
||||
if max_count and len(result) > max_count:
|
||||
raise SearchError(
|
||||
f"Expected less than {max_count} element(s), found {len(result)} elements\n{result}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def find_child(
|
||||
tree: Union[T, DAGNodeT],
|
||||
condition: Callable[[Union[T, DAGNodeT]], bool],
|
||||
) -> Union[T, DAGNodeT]:
|
||||
"""
|
||||
Search children for *single node* matching condition (callable function).
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_child
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_child(root, lambda node: node.age > 62)
|
||||
Node(/a/b, age=65)
|
||||
|
||||
Args:
|
||||
tree (BaseNode/DAGNode): tree to search for its child
|
||||
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
|
||||
|
||||
Returns:
|
||||
(BaseNode/DAGNode)
|
||||
"""
|
||||
result = find_children(tree, condition, max_count=1)
|
||||
if result:
|
||||
return result[0]
|
||||
|
||||
|
||||
def find_child_by_name(
|
||||
tree: Union[NodeT, DAGNodeT], name: str
|
||||
) -> Union[NodeT, DAGNodeT]:
|
||||
"""
|
||||
Search tree for single node matching name attribute.
|
||||
|
||||
Examples:
|
||||
>>> from bigtree import Node, find_child_by_name
|
||||
>>> root = Node("a", age=90)
|
||||
>>> b = Node("b", age=65, parent=root)
|
||||
>>> c = Node("c", age=60, parent=root)
|
||||
>>> d = Node("d", age=40, parent=c)
|
||||
>>> find_child_by_name(root, "c")
|
||||
Node(/a/c, age=60)
|
||||
>>> find_child_by_name(c, "d")
|
||||
Node(/a/c/d, age=40)
|
||||
|
||||
Args:
|
||||
tree (Node/DAGNode): tree to search, parent node
|
||||
name (str): value to match for name attribute, child node
|
||||
|
||||
Returns:
|
||||
(Node/DAGNode)
|
||||
"""
|
||||
return find_child(tree, lambda node: node.node_name == name)
|
||||
Reference in New Issue
Block a user