halib 0.2.28__tar.gz → 0.2.30__tar.gz

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.
Files changed (58) hide show
  1. {halib-0.2.28 → halib-0.2.30}/PKG-INFO +7 -1
  2. {halib-0.2.28 → halib-0.2.30}/README.md +6 -0
  3. {halib-0.2.28 → halib-0.2.30}/halib/__init__.py +4 -0
  4. {halib-0.2.28 → halib-0.2.30}/halib/common/common.py +122 -3
  5. halib-0.2.30/halib/utils/dict.py +317 -0
  6. {halib-0.2.28 → halib-0.2.30}/halib/utils/slack.py +3 -1
  7. {halib-0.2.28 → halib-0.2.30}/halib.egg-info/PKG-INFO +7 -1
  8. {halib-0.2.28 → halib-0.2.30}/setup.py +1 -1
  9. halib-0.2.28/halib/utils/dict.py +0 -202
  10. {halib-0.2.28 → halib-0.2.30}/.gitignore +0 -0
  11. {halib-0.2.28 → halib-0.2.30}/GDriveFolder.txt +0 -0
  12. {halib-0.2.28 → halib-0.2.30}/LICENSE.txt +0 -0
  13. {halib-0.2.28 → halib-0.2.30}/MANIFEST.in +0 -0
  14. {halib-0.2.28 → halib-0.2.30}/halib/common/__init__.py +0 -0
  15. {halib-0.2.28 → halib-0.2.30}/halib/common/rich_color.py +0 -0
  16. {halib-0.2.28 → halib-0.2.30}/halib/exp/__init__.py +0 -0
  17. {halib-0.2.28 → halib-0.2.30}/halib/exp/core/__init__.py +0 -0
  18. {halib-0.2.28 → halib-0.2.30}/halib/exp/core/base_config.py +0 -0
  19. {halib-0.2.28 → halib-0.2.30}/halib/exp/core/base_exp.py +0 -0
  20. {halib-0.2.28 → halib-0.2.30}/halib/exp/core/param_gen.py +0 -0
  21. {halib-0.2.28 → halib-0.2.30}/halib/exp/core/wandb_op.py +0 -0
  22. {halib-0.2.28 → halib-0.2.30}/halib/exp/data/__init__.py +0 -0
  23. {halib-0.2.28 → halib-0.2.30}/halib/exp/data/dataclass_util.py +0 -0
  24. {halib-0.2.28 → halib-0.2.30}/halib/exp/data/dataset.py +0 -0
  25. {halib-0.2.28 → halib-0.2.30}/halib/exp/data/torchloader.py +0 -0
  26. {halib-0.2.28 → halib-0.2.30}/halib/exp/perf/__init__.py +0 -0
  27. {halib-0.2.28 → halib-0.2.30}/halib/exp/perf/flop_calc.py +0 -0
  28. {halib-0.2.28 → halib-0.2.30}/halib/exp/perf/gpu_mon.py +0 -0
  29. {halib-0.2.28 → halib-0.2.30}/halib/exp/perf/perfcalc.py +0 -0
  30. {halib-0.2.28 → halib-0.2.30}/halib/exp/perf/perfmetrics.py +0 -0
  31. {halib-0.2.28 → halib-0.2.30}/halib/exp/perf/perftb.py +0 -0
  32. {halib-0.2.28 → halib-0.2.30}/halib/exp/perf/profiler.py +0 -0
  33. {halib-0.2.28 → halib-0.2.30}/halib/exp/viz/__init__.py +0 -0
  34. {halib-0.2.28 → halib-0.2.30}/halib/exp/viz/plot.py +0 -0
  35. {halib-0.2.28 → halib-0.2.30}/halib/filetype/__init__.py +0 -0
  36. {halib-0.2.28 → halib-0.2.30}/halib/filetype/csvfile.py +0 -0
  37. {halib-0.2.28 → halib-0.2.30}/halib/filetype/ipynb.py +0 -0
  38. {halib-0.2.28 → halib-0.2.30}/halib/filetype/jsonfile.py +0 -0
  39. {halib-0.2.28 → halib-0.2.30}/halib/filetype/textfile.py +0 -0
  40. {halib-0.2.28 → halib-0.2.30}/halib/filetype/videofile.py +0 -0
  41. {halib-0.2.28 → halib-0.2.30}/halib/filetype/yamlfile.py +0 -0
  42. {halib-0.2.28 → halib-0.2.30}/halib/online/__init__.py +0 -0
  43. {halib-0.2.28 → halib-0.2.30}/halib/online/gdrive.py +0 -0
  44. {halib-0.2.28 → halib-0.2.30}/halib/online/gdrive_mkdir.py +0 -0
  45. {halib-0.2.28 → halib-0.2.30}/halib/online/projectmake.py +0 -0
  46. {halib-0.2.28 → halib-0.2.30}/halib/online/tele_noti.py +0 -0
  47. {halib-0.2.28 → halib-0.2.30}/halib/system/__init__.py +0 -0
  48. {halib-0.2.28 → halib-0.2.30}/halib/system/_list_pc.csv +0 -0
  49. {halib-0.2.28 → halib-0.2.30}/halib/system/cmd.py +0 -0
  50. {halib-0.2.28 → halib-0.2.30}/halib/system/filesys.py +0 -0
  51. {halib-0.2.28 → halib-0.2.30}/halib/system/path.py +0 -0
  52. {halib-0.2.28 → halib-0.2.30}/halib/utils/__init__.py +0 -0
  53. {halib-0.2.28 → halib-0.2.30}/halib/utils/list.py +0 -0
  54. {halib-0.2.28 → halib-0.2.30}/halib.egg-info/SOURCES.txt +0 -0
  55. {halib-0.2.28 → halib-0.2.30}/halib.egg-info/dependency_links.txt +0 -0
  56. {halib-0.2.28 → halib-0.2.30}/halib.egg-info/requires.txt +0 -0
  57. {halib-0.2.28 → halib-0.2.30}/halib.egg-info/top_level.txt +0 -0
  58. {halib-0.2.28 → halib-0.2.30}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.28
3
+ Version: 0.2.30
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -57,6 +57,12 @@ Dynamic: summary
57
57
 
58
58
  ## v0.2.x (Experiment & Core Updates)
59
59
 
60
+ ### **v0.2.30**
61
+
62
+ - ✨ **New Feature:**: add `common.common.log_func` as decorator to log function entry, exit, with execution time and arguments.
63
+
64
+ - 🚀 **Improvement:**: enhance `utils.dict.DictUtils` with `deep_exclude/include` and `prune` function
65
+
60
66
  ### **v0.2.28**
61
67
 
62
68
  - ✨ **New Feature:** Implement `utils.slack.SlackUtils` class for managing Slack channel message deletion
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## v0.2.x (Experiment & Core Updates)
4
4
 
5
+ ### **v0.2.30**
6
+
7
+ - ✨ **New Feature:**: add `common.common.log_func` as decorator to log function entry, exit, with execution time and arguments.
8
+
9
+ - 🚀 **Improvement:**: enhance `utils.dict.DictUtils` with `deep_exclude/include` and `prune` function
10
+
5
11
  ### **v0.2.28**
6
12
 
7
13
  - ✨ **New Feature:** Implement `utils.slack.SlackUtils` class for managing Slack channel message deletion
@@ -10,6 +10,7 @@ __all__ = [
10
10
  "fs",
11
11
  "inspect",
12
12
  "load_yaml",
13
+ "log_func",
13
14
  "logger",
14
15
  "norm_str",
15
16
  "now_str",
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "omegaconf",
18
19
  "OmegaConf",
19
20
  "os",
21
+ "pad_string",
20
22
  "pd",
21
23
  "plt",
22
24
  "pprint_box",
@@ -66,6 +68,8 @@ from .common.common import (
66
68
  pprint_local_path,
67
69
  pprint_stack_trace,
68
70
  tcuda,
71
+ log_func,
72
+ pad_string,
69
73
  )
70
74
 
71
75
  # for log
@@ -14,6 +14,10 @@ from pathlib import Path, PureWindowsPath
14
14
  from typing import Optional
15
15
  from loguru import logger
16
16
 
17
+ import functools
18
+ from typing import Callable, List, Literal, Union
19
+ import time
20
+ import math
17
21
 
18
22
  console = Console()
19
23
 
@@ -48,6 +52,78 @@ def now_str(sep_date_time="."):
48
52
  return now_string
49
53
 
50
54
 
55
+ def pad_string(
56
+ text: str,
57
+ target_width: Union[int, float] = -1,
58
+ pad_char: str = ".",
59
+ pad_sides: List[Literal["left", "right"]] = ["left", "right"], # type: ignore
60
+ ) -> str:
61
+ """
62
+ Pads a string to a specific width or a relative multiplier width.
63
+
64
+ Args:
65
+ text: The input string.
66
+ target_width:
67
+ - If int (e.g., 20): The exact total length of the resulting string.
68
+ - If float (e.g., 1.5): Multiplies original length (must be >= 1.0).
69
+ (e.g., length 10 * 1.5 = target width 15).
70
+ pad_char: The character to use for padding.
71
+ pad_sides: A list containing "left", "right", or both.
72
+ """
73
+ current_len = len(text)
74
+
75
+ # 1. Calculate the final integer target width
76
+ if isinstance(target_width, float):
77
+ if target_width < 1.0:
78
+ raise ValueError(f"Float target_width must be >= 1.0, got {target_width}")
79
+ # Use math.ceil to ensure we don't under-pad (e.g. 1.5 * 5 = 7.5 -> 8)
80
+ final_width = math.ceil(current_len * target_width)
81
+ else:
82
+ final_width = target_width
83
+
84
+ # 2. Return early if no padding needed
85
+ if current_len >= final_width:
86
+ return text
87
+
88
+ # 3. Calculate total padding needed
89
+ padding_needed = final_width - current_len
90
+
91
+ # CASE 1: Pad Both Sides (Center)
92
+ if "left" in pad_sides and "right" in pad_sides:
93
+ left_pad_count = padding_needed // 2
94
+ right_pad_count = padding_needed - left_pad_count
95
+ return (pad_char * left_pad_count) + text + (pad_char * right_pad_count)
96
+
97
+ # CASE 2: Pad Left Only (Right Align)
98
+ elif "left" in pad_sides:
99
+ return (pad_char * padding_needed) + text
100
+
101
+ # CASE 3: Pad Right Only (Left Align)
102
+ elif "right" in pad_sides:
103
+ return text + (pad_char * padding_needed)
104
+
105
+ return text
106
+
107
+
108
+ # ==========================================
109
+ # Usage Examples
110
+ # ==========================================
111
+ if __name__ == "__main__":
112
+ s = "Hello"
113
+
114
+ # 1. Default (Both sides / Center)
115
+ print(f"'{pad_string(s, 11)}'")
116
+ # Output: "'***Hello***'"
117
+
118
+ # 2. Left Only
119
+ print(f"'{pad_string(s, 10, '-', ['left'])}'")
120
+ # Output: "'-----Hello'"
121
+
122
+ # 3. Right Only
123
+ print(f"'{pad_string(s, 10, '.', ['right'])}'")
124
+ # Output: "'Hello.....'"
125
+
126
+
51
127
  def norm_str(in_str):
52
128
  # Replace one or more whitespace characters with a single underscore
53
129
  norm_string = re.sub(r"\s+", "_", in_str)
@@ -140,9 +216,12 @@ def pprint_stack_trace(
140
216
  msg = DEFAULT_STACK_TRACE_MSG
141
217
  logger.opt(exception=e).warning(msg)
142
218
  if force_stop:
143
- console.rule("[red]Force Stop Triggered in <halib.common.pprint_stack_trace>[/red]")
219
+ console.rule(
220
+ "[red]Force Stop Triggered in <halib.common.pprint_stack_trace>[/red]"
221
+ )
144
222
  sys.exit(1)
145
223
 
224
+
146
225
  def pprint_local_path(
147
226
  local_path: str, get_wins_path: bool = False, tag: str = ""
148
227
  ) -> str:
@@ -181,20 +260,60 @@ def pprint_local_path(
181
260
  return file_uri
182
261
 
183
262
 
263
+ def log_func(
264
+ func: Optional[Callable] = None, *, log_time: bool = False, log_args: bool = False
265
+ ):
266
+ """
267
+ A decorator that logs the start/end of a function.
268
+ Supports both @log_func and @log_func(log_time=True) usage.
269
+ """
270
+ # 1. HANDLE ARGUMENTS: If called as @log_func(log_time=True), func is None.
271
+ # We return a 'partial' function that remembers the args and waits for the func.
272
+ if func is None:
273
+ return functools.partial(log_func, log_time=log_time, log_args=log_args)
274
+
275
+ # 2. HANDLE DECORATION: If called as @log_func, func is the actual function.
276
+ @functools.wraps(func)
277
+ def wrapper(*args, **kwargs):
278
+ # Safe way to get name (handles partials/lambdas)
279
+ func_name = getattr(func, "__name__", "Unknown_Func")
280
+
281
+ # Note: Ensure 'ConsoleLog' context manager is available in your scope
282
+ with ConsoleLog(func_name):
283
+ start = time.perf_counter()
284
+ try:
285
+ result = func(*args, **kwargs)
286
+ finally:
287
+ # We use finally to ensure logging happens even if func crashes
288
+ end = time.perf_counter()
289
+
290
+ if log_time or log_args:
291
+
292
+ console.print(pad_string(f"Func <{func_name}> summary", 80))
293
+ if log_time:
294
+ console.print(f"{func_name} took {end - start:.6f} seconds")
295
+ if log_args:
296
+ console.print(f"Args: {args}, Kwargs: {kwargs}")
297
+
298
+ return result
299
+
300
+ return wrapper
301
+
302
+
184
303
  def tcuda():
185
304
  NOT_INSTALLED = "Not Installed"
186
305
  GPU_AVAILABLE = "GPU(s) Available"
187
306
  ls_lib = ["torch", "tensorflow"]
188
307
  lib_stats = {lib: NOT_INSTALLED for lib in ls_lib}
189
308
  for lib in ls_lib:
190
- spec = importlib.util.find_spec(lib)
309
+ spec = importlib.util.find_spec(lib) # ty:ignore[possibly-missing-attribute]
191
310
  if spec:
192
311
  if lib == "torch":
193
312
  import torch
194
313
 
195
314
  lib_stats[lib] = str(torch.cuda.device_count()) + " " + GPU_AVAILABLE
196
315
  elif lib == "tensorflow":
197
- import tensorflow as tf
316
+ import tensorflow as tf # type: ignore
198
317
 
199
318
  lib_stats[lib] = (
200
319
  str(len(tf.config.list_physical_devices("GPU")))
@@ -0,0 +1,317 @@
1
+ from future.utils.surrogateescape import fn
2
+ import copy
3
+ import json
4
+ import hashlib
5
+ from rich.pretty import pprint
6
+ from typing import Dict, Any, Callable, Optional, List, Tuple
7
+
8
+
9
+ class DictUtils:
10
+ """
11
+ General-purpose dictionary manipulation utilities.
12
+ """
13
+
14
+ @staticmethod
15
+ def flatten(
16
+ d: Dict[str, Any],
17
+ parent_key: str = "",
18
+ sep: str = ".",
19
+ is_leaf_predicate: Optional[Callable[[Any], bool]] = None,
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Recursively flattens a nested dictionary.
23
+
24
+ Args:
25
+ d: The dictionary to flatten.
26
+ parent_key: Prefix for keys (used during recursion).
27
+ sep: Separator for dot-notation keys.
28
+ is_leaf_predicate: Optional function that returns True if a value should
29
+ be treated as a leaf (value) rather than a branch to recurse.
30
+ Useful if you have dicts you don't want flattened.
31
+ """
32
+ items = []
33
+ for k, v in d.items():
34
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
35
+
36
+ # Check if we should treat this as a leaf (custom logic)
37
+ if is_leaf_predicate and is_leaf_predicate(v):
38
+ items.append((new_key, v))
39
+ # Standard recursion
40
+ elif isinstance(v, dict):
41
+ items.extend(
42
+ DictUtils.flatten(
43
+ v, new_key, sep=sep, is_leaf_predicate=is_leaf_predicate
44
+ ).items()
45
+ )
46
+ else:
47
+ items.append((new_key, v))
48
+ return dict(items)
49
+
50
+ @staticmethod
51
+ def unflatten(flat_dict: Dict[str, Any], sep: str = ".") -> Dict[str, Any]:
52
+ """
53
+ Converts flat dot-notation keys back to nested dictionaries.
54
+ e.g., {'a.b': 1} -> {'a': {'b': 1}}
55
+ """
56
+ nested = {}
57
+ for key, value in flat_dict.items():
58
+ DictUtils.deep_set(nested, key, value, sep=sep)
59
+ return nested
60
+
61
+ @staticmethod
62
+ def deep_update(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
63
+ """
64
+ Recursively merges 'update' dict into 'base' dict.
65
+
66
+ Unlike the standard `dict.update()`, which replaces nested dictionaries entirely,
67
+ this method enters nested dictionaries and updates them key-by-key. This preserves
68
+ existing keys in 'base' that are not present in 'update'.
69
+
70
+ Args:
71
+ base: The original dictionary to modify.
72
+ update: The dictionary containing new values.
73
+
74
+ Returns:
75
+ The modified 'base' dictionary.
76
+
77
+ Example:
78
+ >>> base = {'model': {'name': 'v1', 'dropout': 0.5}}
79
+ >>> new_vals = {'model': {'name': 'v2'}}
80
+ >>> # Standard update would delete 'dropout'. deep_update keeps it:
81
+ >>> DictUtils.deep_update(base, new_vals)
82
+ {'model': {'name': 'v2', 'dropout': 0.5}}
83
+ """
84
+ for k, v in update.items():
85
+ if isinstance(v, dict) and k in base and isinstance(base[k], dict):
86
+ DictUtils.deep_update(base[k], v)
87
+ else:
88
+ base[k] = v
89
+ return base
90
+
91
+ @staticmethod
92
+ def deep_set(d: Dict[str, Any], dot_key: str, value: Any, sep: str = ".") -> None:
93
+ """
94
+ Sets a value in a nested dictionary using a dot-notation key path.
95
+ Automatically creates any missing intermediate dictionaries.
96
+
97
+ Args:
98
+ d: The dictionary to modify.
99
+ dot_key: The path to the value (e.g., "model.backbone.layers").
100
+ value: The value to set.
101
+ sep: The separator used in the key (default is ".").
102
+
103
+ Example:
104
+ >>> cfg = {}
105
+ >>> DictUtils.deep_set(cfg, "a.b.c", 10)
106
+ >>> print(cfg)
107
+ {'a': {'b': {'c': 10}}}
108
+ """
109
+ parts = dot_key.split(sep)
110
+ target = d
111
+ for part in parts[:-1]:
112
+ if part not in target:
113
+ target[part] = {}
114
+ target = target[part]
115
+ if not isinstance(target, dict):
116
+ # Handle conflict if a path was previously a value (e.g. overwriting a leaf)
117
+ target = {}
118
+ target[parts[-1]] = value
119
+
120
+ @staticmethod
121
+ def get_unique_hash(input_dict, length=12):
122
+ """
123
+ Returns a unique hash string for a dictionary.
124
+
125
+ :param input_dict: The dictionary params
126
+ :param length: The desired length of the hash string (default 12)
127
+ """
128
+ assert length >= 12, "Hash length must be at least 12 to ensure uniqueness."
129
+ # 1. Sort keys to ensure {a:1, b:2} == {b:2, a:1}
130
+ config_str = json.dumps(input_dict, sort_keys=True)
131
+
132
+ # 2. Generate full SHA-256 hash (64 chars long)
133
+ full_hash = hashlib.sha256(config_str.encode("utf-8")).hexdigest()
134
+
135
+ # 3. Truncate to desired length
136
+ return full_hash[:length]
137
+
138
+ @staticmethod
139
+ def deep_exclude(
140
+ d: Dict[str, Any],
141
+ keys_to_exclude: List[str],
142
+ in_place: bool = False,
143
+ sep: str = ".",
144
+ ) -> Dict[str, Any]:
145
+ """
146
+ Removes keys from a nested dictionary based on a list of dot-notation paths.
147
+
148
+ Args:
149
+ d: The dictionary to filter.
150
+ keys_to_exclude: A list of flattened keys to exclude (e.g., ['model.layers.dropout']).
151
+ in_place: If True, modifies the dictionary directly.
152
+ If False, creates and modifies a deep copy, leaving the original untouched.
153
+ sep: Separator used in the dot-notation keys (default: ".").
154
+
155
+ Returns:
156
+ The modified dictionary (either the original object or the new copy).
157
+
158
+ Example:
159
+ >>> data = {'a': {'b': 1, 'c': 2}}
160
+ >>> DictUtils.deep_exclude(data, ['a.b'], in_place=False)
161
+ {'a': {'c': 2}}
162
+ """
163
+ # 1. Handle the copy logic based on the in_place flag
164
+ if in_place:
165
+ target_dict = d
166
+ else:
167
+ target_dict = copy.deepcopy(d)
168
+
169
+ # 2. Iterate over each dot-notation key we want to delete
170
+ for flat_key in keys_to_exclude:
171
+ parts = flat_key.split(sep)
172
+
173
+ # 3. Traverse to the parent container of the key we want to delete
174
+ current_level = target_dict
175
+ parent_found = True
176
+
177
+ # Loop through path parts up to the second-to-last item (the parent)
178
+ for part in parts[:-1]:
179
+ if isinstance(current_level, dict) and part in current_level:
180
+ current_level = current_level[part]
181
+ else:
182
+ # The path doesn't exist in this dict, safely skip deletion
183
+ parent_found = False
184
+ break
185
+
186
+ # 4. Delete the final key (leaf) if the parent was found
187
+ if parent_found and isinstance(current_level, dict):
188
+ leaf_key = parts[-1]
189
+ if leaf_key in current_level:
190
+ del current_level[leaf_key]
191
+
192
+ return target_dict
193
+
194
+ @staticmethod
195
+ def deep_include(
196
+ d: Dict[str, Any],
197
+ keys_to_include: List[str],
198
+ in_place: bool = False,
199
+ sep: str = ".",
200
+ ) -> Dict[str, Any]:
201
+ """
202
+ Filters a nested dictionary to keep ONLY the specified dot-notation paths.
203
+
204
+ Args:
205
+ d: The dictionary to filter.
206
+ keys_to_include: A list of flattened keys to include (e.g., ['a.b.c']).
207
+ in_place: If True, modifies the original dictionary.
208
+ sep: Separator used in the dot-notation keys.
209
+
210
+ Returns:
211
+ The filtered dictionary.
212
+ """
213
+ # 1. Create a fresh container for the keys we want to preserve
214
+ # Unlike deep_remove, it's often cleaner to build a new dict
215
+ # than to delete everything else.
216
+ new_dict = {}
217
+
218
+ for flat_key in keys_to_include:
219
+ parts = flat_key.split(sep)
220
+
221
+ # Pointers to traverse both dictionaries
222
+ current_source = d
223
+ current_target = new_dict
224
+
225
+ for i, part in enumerate(parts):
226
+ if isinstance(current_source, dict) and part in current_source:
227
+ # Move down the source
228
+ current_source = current_source[part]
229
+
230
+ # If we are at the leaf of the 'keep' path, copy the value
231
+ if i == len(parts) - 1:
232
+ current_target[part] = copy.deepcopy(current_source)
233
+ else:
234
+ # If the path doesn't exist in our new_dict yet, create it
235
+ if part not in current_target or not isinstance(
236
+ current_target[part], dict
237
+ ):
238
+ current_target[part] = {}
239
+ current_target = current_target[part]
240
+ else:
241
+ # The path to keep doesn't exist in the source, skip it
242
+ break
243
+
244
+ # 2. Handle the in_place logic
245
+ if in_place:
246
+ d.clear()
247
+ d.update(new_dict)
248
+ return d
249
+
250
+ return new_dict
251
+
252
+ @staticmethod
253
+ def apply_exclusion_mask(
254
+ d: Dict[str, Any],
255
+ config_mask: Dict[str, Any],
256
+ in_place: bool = False,
257
+ sep: str = ".",
258
+ ) -> Dict[str, Any]:
259
+ """
260
+ Uses a dictionary 'mask' to define what to throw away.
261
+ """
262
+ # Assuming your DictUtils.flatten returns a dict of {path: value}
263
+ flatten_dict = DictUtils.flatten(config_mask, sep=sep)
264
+ paths_to_exclude = list(flatten_dict.keys())
265
+ return DictUtils.deep_exclude(d, paths_to_exclude, in_place=in_place, sep=sep)
266
+
267
+ @staticmethod
268
+ def apply_inclusion_mask(
269
+ d: Dict[str, Any],
270
+ config_mask: Dict[str, Any],
271
+ in_place: bool = False,
272
+ sep: str = ".",
273
+ ) -> Dict[str, Any]:
274
+ """
275
+ Renamed from 'deep_keep_by_config'.
276
+ Uses a dictionary 'mask' to define what to allow.
277
+ """
278
+ flatten_dict = DictUtils.flatten(config_mask, sep=sep)
279
+ paths_to_include = list(flatten_dict.keys())
280
+ return DictUtils.deep_include(d, paths_to_include, in_place=in_place, sep=sep)
281
+
282
+ @staticmethod
283
+ def prune(d: Any, prune_values: Tuple[Any, ...] = (None, {}, [], "")) -> Any:
284
+ """
285
+ Recursively removes keys where values match any item in 'prune_values'.
286
+
287
+ Args:
288
+ d: The dictionary or list to clean.
289
+ prune_values: A tuple of values to be removed.
290
+ Default is (None, {}, [], "") which removes all empty types.
291
+ Pass specific values (e.g., ({}, "")) to keep None or [].
292
+
293
+ Returns:
294
+ The cleaned structure.
295
+ """
296
+ if isinstance(d, dict):
297
+ new_dict = {}
298
+ for k, v in d.items():
299
+ # 1. Recursively clean children first
300
+ cleaned_v = DictUtils.prune(v, prune_values)
301
+
302
+ # 2. Check if the CLEANED value is in the delete list
303
+ # We use strict check to ensure we don't delete 0 or False unless requested
304
+ if cleaned_v not in prune_values:
305
+ new_dict[k] = cleaned_v
306
+ return new_dict
307
+
308
+ elif isinstance(d, list):
309
+ new_list = []
310
+ for v in d:
311
+ cleaned_v = DictUtils.prune(v, prune_values)
312
+ if cleaned_v not in prune_values:
313
+ new_list.append(cleaned_v)
314
+ return new_list
315
+
316
+ else:
317
+ return d
@@ -3,7 +3,9 @@ from slack_sdk import WebClient
3
3
  from slack_sdk.errors import SlackApiError
4
4
  from rich.pretty import pprint
5
5
 
6
-
6
+ """
7
+ Utilities for interacting with Slack for experiment notification via Wandb Logger.
8
+ """
7
9
  class SlackUtils:
8
10
  _instance = None
9
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.28
3
+ Version: 0.2.30
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -57,6 +57,12 @@ Dynamic: summary
57
57
 
58
58
  ## v0.2.x (Experiment & Core Updates)
59
59
 
60
+ ### **v0.2.30**
61
+
62
+ - ✨ **New Feature:**: add `common.common.log_func` as decorator to log function entry, exit, with execution time and arguments.
63
+
64
+ - 🚀 **Improvement:**: enhance `utils.dict.DictUtils` with `deep_exclude/include` and `prune` function
65
+
60
66
  ### **v0.2.28**
61
67
 
62
68
  - ✨ **New Feature:** Implement `utils.slack.SlackUtils` class for managing Slack channel message deletion
@@ -8,7 +8,7 @@ with open("requirements.txt", "r", encoding="utf-8") as f:
8
8
 
9
9
  setuptools.setup(
10
10
  name="halib",
11
- version="0.2.28",
11
+ version="0.2.30",
12
12
  author="Hoang Van Ha",
13
13
  author_email="hoangvanhauit@gmail.com",
14
14
  description="Small library for common tasks",
@@ -1,202 +0,0 @@
1
- from typing import Dict, Any, Callable, Optional
2
- from rich.pretty import pprint
3
- import json
4
- import hashlib
5
-
6
-
7
- class DictUtils:
8
- """
9
- General-purpose dictionary manipulation utilities.
10
- """
11
-
12
- @staticmethod
13
- def flatten(
14
- d: Dict[str, Any],
15
- parent_key: str = "",
16
- sep: str = ".",
17
- is_leaf_predicate: Optional[Callable[[Any], bool]] = None,
18
- ) -> Dict[str, Any]:
19
- """
20
- Recursively flattens a nested dictionary.
21
-
22
- Args:
23
- d: The dictionary to flatten.
24
- parent_key: Prefix for keys (used during recursion).
25
- sep: Separator for dot-notation keys.
26
- is_leaf_predicate: Optional function that returns True if a value should
27
- be treated as a leaf (value) rather than a branch to recurse.
28
- Useful if you have dicts you don't want flattened.
29
- """
30
- items = []
31
- for k, v in d.items():
32
- new_key = f"{parent_key}{sep}{k}" if parent_key else k
33
-
34
- # Check if we should treat this as a leaf (custom logic)
35
- if is_leaf_predicate and is_leaf_predicate(v):
36
- items.append((new_key, v))
37
- # Standard recursion
38
- elif isinstance(v, dict):
39
- items.extend(
40
- DictUtils.flatten(
41
- v, new_key, sep=sep, is_leaf_predicate=is_leaf_predicate
42
- ).items()
43
- )
44
- else:
45
- items.append((new_key, v))
46
- return dict(items)
47
-
48
- @staticmethod
49
- def unflatten(flat_dict: Dict[str, Any], sep: str = ".") -> Dict[str, Any]:
50
- """
51
- Converts flat dot-notation keys back to nested dictionaries.
52
- e.g., {'a.b': 1} -> {'a': {'b': 1}}
53
- """
54
- nested = {}
55
- for key, value in flat_dict.items():
56
- DictUtils.deep_set(nested, key, value, sep=sep)
57
- return nested
58
-
59
- @staticmethod
60
- def deep_update(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
61
- """
62
- Recursively merges 'update' dict into 'base' dict.
63
-
64
- Unlike the standard `dict.update()`, which replaces nested dictionaries entirely,
65
- this method enters nested dictionaries and updates them key-by-key. This preserves
66
- existing keys in 'base' that are not present in 'update'.
67
-
68
- Args:
69
- base: The original dictionary to modify.
70
- update: The dictionary containing new values.
71
-
72
- Returns:
73
- The modified 'base' dictionary.
74
-
75
- Example:
76
- >>> base = {'model': {'name': 'v1', 'dropout': 0.5}}
77
- >>> new_vals = {'model': {'name': 'v2'}}
78
- >>> # Standard update would delete 'dropout'. deep_update keeps it:
79
- >>> DictUtils.deep_update(base, new_vals)
80
- {'model': {'name': 'v2', 'dropout': 0.5}}
81
- """
82
- for k, v in update.items():
83
- if isinstance(v, dict) and k in base and isinstance(base[k], dict):
84
- DictUtils.deep_update(base[k], v)
85
- else:
86
- base[k] = v
87
- return base
88
-
89
- @staticmethod
90
- def deep_set(d: Dict[str, Any], dot_key: str, value: Any, sep: str = ".") -> None:
91
- """
92
- Sets a value in a nested dictionary using a dot-notation key path.
93
- Automatically creates any missing intermediate dictionaries.
94
-
95
- Args:
96
- d: The dictionary to modify.
97
- dot_key: The path to the value (e.g., "model.backbone.layers").
98
- value: The value to set.
99
- sep: The separator used in the key (default is ".").
100
-
101
- Example:
102
- >>> cfg = {}
103
- >>> DictUtils.deep_set(cfg, "a.b.c", 10)
104
- >>> print(cfg)
105
- {'a': {'b': {'c': 10}}}
106
- """
107
- parts = dot_key.split(sep)
108
- target = d
109
- for part in parts[:-1]:
110
- if part not in target:
111
- target[part] = {}
112
- target = target[part]
113
- if not isinstance(target, dict):
114
- # Handle conflict if a path was previously a value (e.g. overwriting a leaf)
115
- target = {}
116
- target[parts[-1]] = value
117
-
118
- @staticmethod
119
- def get_unique_hash(input_dict, length=12):
120
- """
121
- Returns a unique hash string for a dictionary.
122
-
123
- :param input_dict: The dictionary params
124
- :param length: The desired length of the hash string (default 12)
125
- """
126
- assert length >= 12, "Hash length must be at least 12 to ensure uniqueness."
127
- # 1. Sort keys to ensure {a:1, b:2} == {b:2, a:1}
128
- config_str = json.dumps(input_dict, sort_keys=True)
129
-
130
- # 2. Generate full SHA-256 hash (64 chars long)
131
- full_hash = hashlib.sha256(config_str.encode("utf-8")).hexdigest()
132
-
133
- # 3. Truncate to desired length
134
- return full_hash[:length]
135
-
136
-
137
- def test_update():
138
- # --- Setup ---
139
- base_config = {
140
- "model": {
141
- "name": "ResNet50",
142
- "layers": 50,
143
- "details": {
144
- "activation": "relu",
145
- "dropout": 0.5, # <--- We want to keep this
146
- },
147
- },
148
- "epochs": 10,
149
- }
150
-
151
- new_settings = {
152
- "model": {"details": {"activation": "gelu"}} # <--- We only want to change this
153
- }
154
-
155
- b1 = base_config.copy()
156
- b2 = base_config.copy()
157
- n1 = new_settings.copy()
158
- n2 = new_settings.copy()
159
-
160
- pprint("Base Config:")
161
- pprint(base_config)
162
- pprint("New Settings:")
163
- pprint(new_settings)
164
- print("*" * 40)
165
- pprint(
166
- "Task: Update base_config with new_settings, preserving unspecified nested keys."
167
- )
168
- print("*" * 40)
169
-
170
- # --- Standard Update (The Problem) ---
171
- pprint("Normal Update Result:")
172
- b1.update(n1)
173
- pprint(b1) # type: ignore[return-value]
174
-
175
- # --- Deep Update (The Solution) ---
176
- pprint("Deep Update Result:")
177
- pprint(DictUtils.deep_update(b2, n2))
178
-
179
-
180
- def test_hash():
181
- # --- Usage ---
182
- cfg1 = {"learning_rate": 0.01, "batch_size": 32, "optimizer": "adam"}
183
- cfg1_shuffle = {
184
- "batch_size": 32,
185
- "optimizer": "adam",
186
- "learning_rate": 0.01,
187
- }
188
- cfg2 = {"learning_rate": 0.02, "batch_size": 32, "optimizer": "adam"}
189
- hash1 = DictUtils.get_unique_hash(cfg1)
190
- hash2 = DictUtils.get_unique_hash(cfg1_shuffle)
191
- hash3 = DictUtils.get_unique_hash(cfg2)
192
- pprint(f"Config 1 Hash: {hash1}")
193
- pprint(f"Config 1_shuffle Hash: {hash2}")
194
- pprint(f"Config 2 Hash: {hash3}")
195
-
196
- assert hash1 == hash2, "Hashes should match for identical dicts."
197
- assert hash1 != hash3, "Hashes should differ for different dicts."
198
-
199
-
200
- if __name__ == "__main__":
201
- test_update()
202
- test_hash()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes