iker-python-common 1.0.62__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,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.62
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
7
7
  Classifier: Programming Language :: Python :: 3.14
8
8
  Requires-Python: <3.15,>=3.12
9
9
  Requires-Dist: asyncpg>=0.30
10
- Requires-Dist: docker>=7.1
11
10
  Requires-Dist: numpy>=2.3
12
11
  Requires-Dist: psycopg>=3.2
13
12
  Requires-Dist: pymysql>=1.1
@@ -4,21 +4,20 @@ iker/common/utils/argutils.py,sha256=hMLNqdZs_Kjc2hw4Npm6N47RivP6JRNzCKIbJr1jYy8
4
4
  iker/common/utils/config.py,sha256=z8rLqli961A-qAV9EaELp-pKuhNUNaq1Btdv-uwG7_I,4690
5
5
  iker/common/utils/csv.py,sha256=_V9OUrKcojec2L-hWagEIVnL2uvGjyJAFTrD7tHNr48,7573
6
6
  iker/common/utils/dbutils.py,sha256=09DgvfPVDCPXwOAO_FTynLXhSq--ZzRz2fCQ6vJ5qqk,16151
7
- iker/common/utils/dockerutils.py,sha256=n2WuzXaZB6_WocSljvPOnfExSIjIHRUbuWp2oBbaPKQ,8004
8
7
  iker/common/utils/dtutils.py,sha256=86vbaa4pgcBWERZvTfJ92PKB3IimxP6tf0O11ho2Ffk,12554
9
8
  iker/common/utils/funcutils.py,sha256=4AkkvK9_Z2tgk1-Sp6-vLLVhI15cIgN9xW58QqL5QL4,7780
9
+ iker/common/utils/iterutils.py,sha256=l-FqaVUiL7WVkei7hYqWJqO5Ptcwe1T5CZ6nuIVNi4w,30884
10
10
  iker/common/utils/jsonutils.py,sha256=AkziMAYVQDODHRqZC-c1x7VqI2hHY3Kxrw7gmoss8mU,18527
11
11
  iker/common/utils/logger.py,sha256=FJaai6Sbchy4wKHcUMUCrrkBcXvIxq4qByERZ_TJBps,3881
12
12
  iker/common/utils/numutils.py,sha256=p6Rz1qyCcUru3v1zDy2PM-nds2NWJdL5A_vLmG-kswk,4294
13
- iker/common/utils/randutils.py,sha256=Sxf852B18CJ-MfrEDsv1ROO_brmz79dRZ4jpJiH65v4,12843
13
+ iker/common/utils/randutils.py,sha256=D9bVAeHnLNkG8aZ2piWJgixTjXe0jl-GCVe4QgeFmsk,12844
14
14
  iker/common/utils/retry.py,sha256=H9lR6pp_jzgOwKTM-dOWIddjTlQbK-ijcwuDmVvurZM,8938
15
- iker/common/utils/sequtils.py,sha256=Wc8RcbNjVYSJYZv_07SOKWfYjhmGWz9_RXWbG2-tE1o,25060
16
- iker/common/utils/shutils.py,sha256=dUm1Y7m8u1Ri_R5598oQJsxwgQaBnVzhtpcsL7_Vzp0,7916
15
+ iker/common/utils/shutils.py,sha256=DFwWdnGdAZEuwx6GDjFzPkMtNCFURjslOFUNs9FhrpA,7917
17
16
  iker/common/utils/span.py,sha256=u_KuWi2U7QDMUotl4AeW2_57ItL3YhVDSeCwaOiFDvs,5963
18
17
  iker/common/utils/strutils.py,sha256=Tu_qFeH3K-SfwvMxdrZAc9iLPV8ZmtX4ntyyFGNslf8,5094
19
18
  iker/common/utils/testutils.py,sha256=2VieV5yeCDntSKQSpIeyqRT8BZmZYE_ArMeQz3g7fXY,5568
20
19
  iker/common/utils/typeutils.py,sha256=RVkYkFRgDrx77OHFH7PavMV0AIB0S8ly40rs4g7JWE4,8220
21
- iker_python_common-1.0.62.dist-info/METADATA,sha256=0xFB0M0zbyGoFcUy9Y-lfZGzNDbAhtW0574bl8XS7XU,894
22
- iker_python_common-1.0.62.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- iker_python_common-1.0.62.dist-info/top_level.txt,sha256=4_B8Prfc_lxFafFYTQThIU1ZqOYQ4pHHHnJ_fQ_oHs8,5
24
- iker_python_common-1.0.62.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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,236 +0,0 @@
1
- import contextlib
2
- import dataclasses
3
- import re
4
- from collections.abc import Generator, Iterator
5
- from typing import Any
6
-
7
- import docker
8
- import docker.errors
9
- import docker.models.containers
10
- import docker.models.images
11
- import requests.exceptions
12
-
13
- from iker.common.utils import logger
14
- from iker.common.utils.strutils import parse_int_or, trim_to_empty
15
-
16
- __all__ = [
17
- "ImageName",
18
- "docker_create_client",
19
- "docker_build_image",
20
- "docker_get_image",
21
- "docker_pull_image",
22
- "docker_fetch_image",
23
- "docker_run_detached",
24
- ]
25
-
26
-
27
- @dataclasses.dataclass
28
- class ImageName(object):
29
- registry_host: str | None
30
- registry_port: int | None
31
- components: list[str]
32
- tag: str | None
33
-
34
- @property
35
- def registry(self) -> str:
36
- if self.registry_host is None and self.registry_port is None:
37
- return ""
38
- if self.registry_port is None:
39
- return self.registry_host
40
- return f"{self.registry_host}:{self.registry_port}"
41
-
42
- @property
43
- def repository(self) -> str:
44
- return "/".join(self.components)
45
-
46
- @staticmethod
47
- def parse(s: str):
48
-
49
- # Registry absent version
50
- matcher = re.compile(
51
- r"^(?P<components>[a-z0-9]+((__?|-+)[a-z0-9]+)*(/[a-z0-9]+((__?|-+)[a-z0-9]+)*)*)(:(?P<tag>\w[\w.-]{0,127}))?$")
52
- match = matcher.match(s)
53
- if match:
54
- return ImageName(registry_host=None,
55
- registry_port=None,
56
- components=trim_to_empty(match.group("components")).split("/"),
57
- tag=match.group("tag"))
58
-
59
- # Registry present version
60
- matcher = re.compile(
61
- r"^((?P<host>[a-zA-Z0-9.-]+)(:(?P<port>\d+))?/)?(?P<components>[a-z0-9]+((__?|-+)[a-z0-9]+)*(/[a-z0-9]+((__?|-+)[a-z0-9]+)*)*)(:(?P<tag>\w[\w.-]{0,127}))?$")
62
- match = matcher.match(s)
63
- if match:
64
- return ImageName(registry_host=match.group("host"),
65
- registry_port=parse_int_or(match.group("port"), None),
66
- components=trim_to_empty(match.group("components")).split("/"),
67
- tag=match.group("tag"))
68
-
69
- return None
70
-
71
-
72
- def docker_create_client(
73
- registry: str,
74
- username: str,
75
- password: str,
76
- ) -> contextlib.AbstractContextManager[docker.DockerClient]:
77
- try:
78
- client = docker.DockerClient()
79
- client.login(registry=registry, username=username, password=password, reauth=True)
80
- return contextlib.closing(client)
81
- except docker.errors.APIError:
82
- logger.exception("Failed to login Docker server <%s>", registry)
83
- raise
84
-
85
-
86
- def docker_build_image(
87
- client: docker.DockerClient,
88
- tag: str,
89
- path: str,
90
- dockerfile: str,
91
- build_args: dict[str, str],
92
- ) -> tuple[docker.models.images.Image, Iterator[dict[str, str]]]:
93
- try:
94
- return client.images.build(tag=tag,
95
- path=path,
96
- dockerfile=dockerfile,
97
- buildargs=build_args,
98
- rm=True,
99
- forcerm=True,
100
- nocache=True)
101
-
102
- except docker.errors.BuildError:
103
- logger.exception("Failed to build image <%s>", tag)
104
- raise
105
- except docker.errors.APIError:
106
- logger.exception("Docker server returns an error while building image <%s>", tag)
107
- raise
108
- except Exception:
109
- logger.exception("Unexpected error occurred while building image <%s>", tag)
110
- raise
111
-
112
-
113
- def docker_get_image(
114
- client: docker.DockerClient,
115
- image: str,
116
- ) -> docker.models.images.Image:
117
- try:
118
- return client.images.get(image)
119
- except docker.errors.ImageNotFound:
120
- logger.exception("Image <%s> is not found from local repository", image)
121
- raise
122
- except docker.errors.APIError:
123
- logger.exception("Docker server returns an error while getting image <%s>", image)
124
- raise
125
- except Exception:
126
- logger.exception("Unexpected error occurred while getting image <%s>", image)
127
- raise
128
-
129
-
130
- def docker_pull_image(
131
- client: docker.DockerClient,
132
- image: str,
133
- fallback_local: bool = False,
134
- ) -> docker.models.images.Image:
135
- try:
136
- return client.images.pull(image)
137
- except docker.errors.ImageNotFound:
138
- if not fallback_local:
139
- logger.exception("Image <%s> is not found from remote repository", image)
140
- raise
141
- logger.warning("Image <%s> is not found from remote repository, try local repository instead", image)
142
- except docker.errors.APIError:
143
- if not fallback_local:
144
- logger.exception("Docker server returns an error while pulling image <%s>", image)
145
- raise
146
- logger.warning("Docker server returns an error while pulling image <%s>, try local repository instead", image)
147
- except Exception:
148
- logger.exception("Unexpected error occurred while pulling image <%s>", image)
149
- raise
150
-
151
- return docker_get_image(client, image)
152
-
153
-
154
- def docker_fetch_image(
155
- client: docker.DockerClient,
156
- image: str,
157
- force_pull: bool = False,
158
- ) -> docker.models.images.Image:
159
- if force_pull:
160
- return docker_pull_image(client, image, fallback_local=True)
161
- else:
162
- try:
163
- return docker_get_image(client, image)
164
- except Exception:
165
- return docker_pull_image(client, image, fallback_local=False)
166
-
167
-
168
- def docker_run_detached(
169
- client: docker.DockerClient,
170
- image: str,
171
- name: str,
172
- command: str | list[str],
173
- volumes: dict[str, dict[str, str]],
174
- environment: dict[str, str],
175
- extra_hosts: dict[str, str],
176
- timeout: int,
177
- **kwargs,
178
- ) -> tuple[dict[str, Any], Any]:
179
- @contextlib.contextmanager
180
- def managed_docker_run(
181
- client: docker.DockerClient,
182
- **kwargs,
183
- ) -> Generator[docker.models.containers.Container, None, None]:
184
- container_model = None
185
- try:
186
- container_model = client.containers.run(**kwargs)
187
- yield container_model
188
- except docker.errors.DockerException:
189
- raise
190
- finally:
191
- if container_model is not None:
192
- try:
193
- if container_model.status != "exited":
194
- try:
195
- container_model.stop()
196
- except docker.errors.DockerException:
197
- pass
198
- container_model.wait()
199
- try:
200
- container_model.remove()
201
- except docker.errors.DockerException:
202
- pass
203
- except Exception:
204
- raise
205
-
206
- try:
207
- run_args = kwargs.copy()
208
- run_args.update(dict(image=image,
209
- name=name,
210
- command=command,
211
- volumes=volumes,
212
- environment=environment,
213
- extra_hosts=extra_hosts,
214
- detach=True))
215
-
216
- with managed_docker_run(client, **run_args) as container_model:
217
- result = container_model.wait(timeout=timeout)
218
- logs = container_model.logs()
219
-
220
- return result, logs
221
-
222
- except requests.exceptions.ReadTimeout:
223
- logger.exception("Running container <%s> of image <%s> exceed the timeout", name, image)
224
- raise
225
- except docker.errors.ImageNotFound:
226
- logger.exception("Image <%s> is not found", image)
227
- raise
228
- except docker.errors.ContainerError:
229
- logger.exception("Failed to run container <%s> of image <%s>", name, image)
230
- raise
231
- except docker.errors.APIError:
232
- logger.exception("Docker server returns an error while running container <%s> of image <%s>", name, image)
233
- raise
234
- except Exception:
235
- logger.exception("Unexpected error occurred while running container <%s> of image <%s>", image)
236
- raise