iker-python-common 1.0.63__py3-none-any.whl → 1.0.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,15 @@ __all__ = [
27
27
  "chunk_between",
28
28
  "chunk_with_key",
29
29
  "merge_chunks",
30
+ "dicttree",
31
+ "dicttree_value",
32
+ "dicttree_subtree",
33
+ "dicttree_children",
34
+ "dicttree_lineage",
35
+ "dicttree_make",
36
+ "dicttree_add",
37
+ "dicttree_remove",
38
+ "dicttree_purge",
30
39
  "Seq",
31
40
  "seq",
32
41
  ]
@@ -539,6 +548,165 @@ def merge_chunks[T](
539
548
  chunk_between(chunks, lambda a, b: not merge_func(a, b)))
540
549
 
541
550
 
551
+ # Dicttree, a tree structure implemented using nested dictionaries,
552
+ # where each node can have an optional value and child nodes.
553
+ type dicttree[K, V] = dict[K, tuple[V | None, dicttree[K, V]]]
554
+
555
+
556
+ def dicttree_value[K, V](tree: dicttree[K, V], path: list[K]) -> V | None:
557
+ """
558
+ Gets the value stored at the specified path in the dicttree.
559
+
560
+ :param tree: the dicttree to retrieve the value from.
561
+ :param path: the list of keys representing the path to the desired value.
562
+ :return: the value at the specified path, or ``None`` if the path does not exist.
563
+ """
564
+ value = None
565
+ for key in path:
566
+ if key not in tree:
567
+ return None
568
+ value, tree = tree[key]
569
+ return value
570
+
571
+
572
+ def dicttree_subtree[K, V](tree: dicttree[K, V], path: list[K]) -> dicttree[K, V] | None:
573
+ """
574
+ Gets the subtree located at the specified path in the dicttree.
575
+
576
+ :param tree: the dicttree to retrieve the subtree from.
577
+ :param path: the list of keys representing the path to the desired subtree.
578
+ :return: the subtree at the specified path, or ``None`` if the path does not exist.
579
+ """
580
+ for key in path:
581
+ if key not in tree:
582
+ return None
583
+ _, tree = tree[key]
584
+ return tree
585
+
586
+
587
+ def dicttree_children[K, V](tree: dicttree[K, V], *, leaves_only: bool = False) -> Generator[V, None, None]:
588
+ """
589
+ Yields all values in the dicttree. If ``leaves_only`` is ``True``, only yields values at leaf nodes.
590
+
591
+ :param tree: the dicttree to traverse.
592
+ :param leaves_only: whether to yield only values at leaf nodes.
593
+ :return: a generator yielding all values in the dicttree.
594
+ """
595
+ for value, subtree in tree.values():
596
+ if value is not None and (not leaves_only or not subtree):
597
+ yield value
598
+ yield from dicttree_children(subtree, leaves_only=leaves_only)
599
+
600
+
601
+ def dicttree_lineage[K, V](tree: dicttree[K, V], path: list[K]) -> Generator[V, None, None]:
602
+ """
603
+ Yields all values along the specified path in the dicttree.
604
+
605
+ :param tree: the dicttree to traverse.
606
+ :param path: the list of keys representing the path to follow.
607
+ :return: a generator yielding all values along the specified path.
608
+ """
609
+ for key in path:
610
+ if key not in tree:
611
+ return
612
+ value, tree = tree[key]
613
+ if value is not None:
614
+ yield value
615
+
616
+
617
+ def dicttree_make[K, V](tree: dicttree[K, V], path: list[K]) -> dicttree[K, V]:
618
+ """
619
+ Ensures that the specified path exists in the dicttree, creating any missing nodes along the way.
620
+
621
+ :param tree: the dicttree to modify.
622
+ :param path: the list of keys representing the path to create.
623
+ :return: the subtree at the end of the created path.
624
+ """
625
+ for key in path:
626
+ tree.setdefault(key, (None, {}))
627
+ _, tree = tree[key]
628
+ return tree
629
+
630
+
631
+ def dicttree_add[K, V](
632
+ tree: dicttree[K, V],
633
+ path: list[K],
634
+ value: V,
635
+ *,
636
+ create_prefix: bool = True,
637
+ overwrite: bool = False,
638
+ ) -> dicttree[K, V]:
639
+ """
640
+ Adds a value at the specified path in the dicttree. If ``create_prefix`` is ``True``, any missing nodes along the
641
+ path are created. If ``overwrite`` is ``False``, raises an error if the path already exists.
642
+
643
+ :param tree: the dicttree to modify.
644
+ :param path: the list of keys representing the path to add the value to.
645
+ :param value: the value to add at the specified path.
646
+ :param create_prefix: whether to create missing nodes along the path.
647
+ :param overwrite: whether to overwrite an existing value at the path.
648
+ :return: the modified dicttree.
649
+ """
650
+ if len(path) == 0:
651
+ raise ValueError("path cannot be empty")
652
+ *prefix, key = path
653
+ if create_prefix:
654
+ subtree = dicttree_make(tree, prefix)
655
+ else:
656
+ subtree = dicttree_subtree(tree, prefix)
657
+ if subtree is None:
658
+ raise ValueError("prefix path does not exist in dicttree")
659
+ if not overwrite and dicttree_value(subtree, [key]) is not None:
660
+ raise ValueError("path already exists in dicttree")
661
+ else:
662
+ subtree[key] = (value, dicttree_subtree(subtree, [key]) or {})
663
+ return tree
664
+
665
+
666
+ def dicttree_remove[K, V](
667
+ tree: dicttree[K, V],
668
+ path: list[K],
669
+ *,
670
+ recursive: bool = False,
671
+ ) -> dicttree[K, V]:
672
+ """
673
+ Removes the value at the specified path in the dicttree. If ``recursive`` is ``True``, removes the entire subtree
674
+ at that path; otherwise, only removes the value, leaving any child nodes intact.
675
+
676
+ :param tree: the dicttree to modify.
677
+ :param path: the list of keys representing the path to remove the value from.
678
+ :param recursive: whether to remove the entire subtree at the path.
679
+ :return: the modified dicttree.
680
+ """
681
+ if len(path) == 0:
682
+ raise ValueError("path cannot be empty")
683
+ *prefix, key = path
684
+ subtree = dicttree_subtree(tree, prefix)
685
+ if subtree is None:
686
+ raise ValueError("prefix path does not exist in dicttree")
687
+ if dicttree_value(subtree, [key]) is None:
688
+ raise ValueError("path does not exist in dicttree")
689
+ else:
690
+ subtree[key] = (None, {} if recursive else dicttree_subtree(subtree, [key]) or {})
691
+ return tree
692
+
693
+
694
+ def dicttree_purge[K, V](tree: dicttree[K, V]) -> dicttree[K, V]:
695
+ """
696
+ Recursively removes all nodes in the dicttree that have no value and no children.
697
+
698
+ :param tree: the dicttree to purge.
699
+ :return: the purged dicttree.
700
+ """
701
+ for key in list(tree.keys()):
702
+ value, subtree = tree[key]
703
+ dicttree_purge(subtree)
704
+ if subtree or value is not None:
705
+ continue
706
+ del tree[key]
707
+ return tree
708
+
709
+
542
710
  class Seq[T](Sequence[T], Sized):
543
711
  def __init__(self, data: Iterable[T]):
544
712
  if isinstance(data, Seq):
@@ -8,8 +8,8 @@ from typing import overload
8
8
 
9
9
  from iker.common.utils.dtutils import dt_utc_max, dt_utc_min
10
10
  from iker.common.utils.funcutils import memorized, singleton
11
+ from iker.common.utils.iterutils import head_or_none
11
12
  from iker.common.utils.jsonutils import JsonType
12
- from iker.common.utils.sequtils import head_or_none
13
13
 
14
14
  __all__ = [
15
15
  "max_int",
@@ -4,7 +4,7 @@ import shutil
4
4
  from typing import Protocol
5
5
 
6
6
  from iker.common.utils import logger
7
- from iker.common.utils.sequtils import last, last_or_none, tail_iter
7
+ from iker.common.utils.iterutils import last, last_or_none, tail_iter
8
8
  from iker.common.utils.strutils import is_empty
9
9
 
10
10
  __all__ = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.63
3
+ Version: 1.0.64
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -6,18 +6,18 @@ iker/common/utils/csv.py,sha256=_V9OUrKcojec2L-hWagEIVnL2uvGjyJAFTrD7tHNr48,7573
6
6
  iker/common/utils/dbutils.py,sha256=09DgvfPVDCPXwOAO_FTynLXhSq--ZzRz2fCQ6vJ5qqk,16151
7
7
  iker/common/utils/dtutils.py,sha256=86vbaa4pgcBWERZvTfJ92PKB3IimxP6tf0O11ho2Ffk,12554
8
8
  iker/common/utils/funcutils.py,sha256=4AkkvK9_Z2tgk1-Sp6-vLLVhI15cIgN9xW58QqL5QL4,7780
9
+ iker/common/utils/iterutils.py,sha256=l-FqaVUiL7WVkei7hYqWJqO5Ptcwe1T5CZ6nuIVNi4w,30884
9
10
  iker/common/utils/jsonutils.py,sha256=AkziMAYVQDODHRqZC-c1x7VqI2hHY3Kxrw7gmoss8mU,18527
10
11
  iker/common/utils/logger.py,sha256=FJaai6Sbchy4wKHcUMUCrrkBcXvIxq4qByERZ_TJBps,3881
11
12
  iker/common/utils/numutils.py,sha256=p6Rz1qyCcUru3v1zDy2PM-nds2NWJdL5A_vLmG-kswk,4294
12
- iker/common/utils/randutils.py,sha256=Sxf852B18CJ-MfrEDsv1ROO_brmz79dRZ4jpJiH65v4,12843
13
+ iker/common/utils/randutils.py,sha256=D9bVAeHnLNkG8aZ2piWJgixTjXe0jl-GCVe4QgeFmsk,12844
13
14
  iker/common/utils/retry.py,sha256=H9lR6pp_jzgOwKTM-dOWIddjTlQbK-ijcwuDmVvurZM,8938
14
- iker/common/utils/sequtils.py,sha256=Wc8RcbNjVYSJYZv_07SOKWfYjhmGWz9_RXWbG2-tE1o,25060
15
- iker/common/utils/shutils.py,sha256=dUm1Y7m8u1Ri_R5598oQJsxwgQaBnVzhtpcsL7_Vzp0,7916
15
+ iker/common/utils/shutils.py,sha256=DFwWdnGdAZEuwx6GDjFzPkMtNCFURjslOFUNs9FhrpA,7917
16
16
  iker/common/utils/span.py,sha256=u_KuWi2U7QDMUotl4AeW2_57ItL3YhVDSeCwaOiFDvs,5963
17
17
  iker/common/utils/strutils.py,sha256=Tu_qFeH3K-SfwvMxdrZAc9iLPV8ZmtX4ntyyFGNslf8,5094
18
18
  iker/common/utils/testutils.py,sha256=2VieV5yeCDntSKQSpIeyqRT8BZmZYE_ArMeQz3g7fXY,5568
19
19
  iker/common/utils/typeutils.py,sha256=RVkYkFRgDrx77OHFH7PavMV0AIB0S8ly40rs4g7JWE4,8220
20
- iker_python_common-1.0.63.dist-info/METADATA,sha256=QUw6lKzGDZRY3Aul1qTE4B_dxtO96Tw0Mvo_acp9Ilw,867
21
- iker_python_common-1.0.63.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
- iker_python_common-1.0.63.dist-info/top_level.txt,sha256=4_B8Prfc_lxFafFYTQThIU1ZqOYQ4pHHHnJ_fQ_oHs8,5
23
- iker_python_common-1.0.63.dist-info/RECORD,,
20
+ iker_python_common-1.0.64.dist-info/METADATA,sha256=mNIZFNquL8xwu6PutfTV3WRrxBzx4-crFq6Lz7r-Kx8,867
21
+ iker_python_common-1.0.64.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ iker_python_common-1.0.64.dist-info/top_level.txt,sha256=4_B8Prfc_lxFafFYTQThIU1ZqOYQ4pHHHnJ_fQ_oHs8,5
23
+ iker_python_common-1.0.64.dist-info/RECORD,,