halib 0.2.28__py3-none-any.whl → 0.2.29__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.
halib/__init__.py CHANGED
@@ -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
halib/common/common.py CHANGED
@@ -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")))
halib/utils/dict.py CHANGED
@@ -1,7 +1,9 @@
1
- from typing import Dict, Any, Callable, Optional
2
- from rich.pretty import pprint
1
+ from future.utils.surrogateescape import fn
2
+ import copy
3
3
  import json
4
4
  import hashlib
5
+ from rich.pretty import pprint
6
+ from typing import Dict, Any, Callable, Optional, List, Tuple
5
7
 
6
8
 
7
9
  class DictUtils:
@@ -133,70 +135,95 @@ class DictUtils:
133
135
  # 3. Truncate to desired length
134
136
  return full_hash[:length]
135
137
 
138
+ @staticmethod
139
+ def deep_remove(
140
+ d: Dict[str, Any],
141
+ keys_to_remove: 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_remove: A list of flattened keys to remove (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).
136
157
 
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()
158
+ Example:
159
+ >>> data = {'a': {'b': 1, 'c': 2}}
160
+ >>> DictUtils.deep_remove(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_remove:
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 prune(d: Any, prune_values: Tuple[Any, ...] = (None, {}, [], "")) -> Any:
196
+ """
197
+ Recursively removes keys where values match any item in 'prune_values'.
198
+
199
+ Args:
200
+ d: The dictionary or list to clean.
201
+ prune_values: A tuple of values to be removed.
202
+ Default is (None, {}, [], "") which removes all empty types.
203
+ Pass specific values (e.g., ({}, "")) to keep None or [].
204
+
205
+ Returns:
206
+ The cleaned structure.
207
+ """
208
+ if isinstance(d, dict):
209
+ new_dict = {}
210
+ for k, v in d.items():
211
+ # 1. Recursively clean children first
212
+ cleaned_v = DictUtils.prune(v, prune_values)
213
+
214
+ # 2. Check if the CLEANED value is in the delete list
215
+ # We use strict check to ensure we don't delete 0 or False unless requested
216
+ if cleaned_v not in prune_values:
217
+ new_dict[k] = cleaned_v
218
+ return new_dict
219
+
220
+ elif isinstance(d, list):
221
+ new_list = []
222
+ for v in d:
223
+ cleaned_v = DictUtils.prune(v, prune_values)
224
+ if cleaned_v not in prune_values:
225
+ new_list.append(cleaned_v)
226
+ return new_list
227
+
228
+ else:
229
+ return d
halib/utils/slack.py CHANGED
@@ -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.29
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.29**
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_remove` 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
@@ -1,4 +1,4 @@
1
- halib/__init__.py,sha256=F2Iq1I1ffjaXJ_LYEqE-MBeUJnwGGE5UHR6M5HVkFkU,1710
1
+ halib/__init__.py,sha256=WIdY-2inwVQ73ZbBnv4XPaxwOJicoBPTkGisHso5mnE,1778
2
2
  halib/common.py,sha256=9hn-IXOlGZODoBHy8U2A0aLgmPEnTeQjbzAVGwXAjwo,4242
3
3
  halib/csvfile.py,sha256=Eoeni0NIbNG3mB5ESWAvNwhJxOjmCaPd1qqYRHImbvk,1567
4
4
  halib/cuda.py,sha256=1bvtBY8QvTWdLaxalzK9wqXPl0Ft3AfhcrebupxGzEA,1010
@@ -17,7 +17,7 @@ halib/textfile.py,sha256=EhVFrit-nRBJx18e6rtIqcE1cSbgsLnMXe_kdhi1EPI,399
17
17
  halib/torchloader.py,sha256=-q9YE-AoHZE1xQX2dgNxdqtucEXYs4sQ22WXdl6EGfI,6500
18
18
  halib/videofile.py,sha256=NTLTZ-j6YD47duw2LN2p-lDQDglYFP1LpEU_0gzHLdI,4737
19
19
  halib/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- halib/common/common.py,sha256=iMoC5Wzj0gn_8GlxEGc0rA5r2HFJ2F1rKy0JpldGtro,5937
20
+ halib/common/common.py,sha256=Ta_4w1k1RUnIXMxvKhsrE5TadRpB9rwqf5qEZcU1oPM,10046
21
21
  halib/common/rich_color.py,sha256=tyK5fl3Dtv1tKsfFzt_5Rco4Fj72QliA-w5aGXaVuqQ,6392
22
22
  halib/exp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  halib/exp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -95,16 +95,16 @@ halib/system/filesys.py,sha256=102J2fkQhmH1_-HQVy2FQ4NOU8LTjMWV3hToT_APtq8,4401
95
95
  halib/system/path.py,sha256=ewiHI76SLFBG5NnlihLpxBbOEDfHibRwKTcLMjEz6Hw,3728
96
96
  halib/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  halib/utils/dataclass_util.py,sha256=rj2IMLlUzbm2OlF5_B2dRTk9njZOaF7tTjYkOsq8uLY,1416
98
- halib/utils/dict.py,sha256=cqyo37IitDgTUgoooO8PTKvPYBuHeCyxTz1QKbYKtLI,6888
98
+ halib/utils/dict.py,sha256=Mag0G3k9KG75ZKnX7bL2Ga4sBxs-icaoX81JZGe-Emw,8630
99
99
  halib/utils/dict_op.py,sha256=wYE6Iw-_CnCWdMg9tpJ2Y2-e2ESkW9FxmdBkZkbUh80,299
100
100
  halib/utils/gpu_mon.py,sha256=vD41_ZnmPLKguuq9X44SB_vwd9JrblO4BDzHLXZhhFY,2233
101
101
  halib/utils/list.py,sha256=bbey9_0IaMXnHx1pudv3C3_WU9uFQEQ5qHPklSN-7o0,498
102
102
  halib/utils/listop.py,sha256=Vpa8_2fI0wySpB2-8sfTBkyi_A4FhoFVVvFiuvW8N64,339
103
- halib/utils/slack.py,sha256=NyiBoH-o653i0tb3KmpbSOmLj0_RACyPgYQWuh4utjs,3007
103
+ halib/utils/slack.py,sha256=2ugWE_eJ0s479ObACJbx7iEu3kjMPD4Rt2hEwuMpuNQ,3099
104
104
  halib/utils/tele_noti.py,sha256=-4WXZelCA4W9BroapkRyIdUu9cUVrcJJhegnMs_WpGU,5928
105
105
  halib/utils/video.py,sha256=zLoj5EHk4SmP9OnoHjO8mLbzPdtq6gQPzTQisOEDdO8,3261
106
- halib-0.2.28.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
107
- halib-0.2.28.dist-info/METADATA,sha256=OLZvOvDQWqp8m_5or_KoqDevjSdjN2mN767yDlYeKLc,7923
108
- halib-0.2.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
109
- halib-0.2.28.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
110
- halib-0.2.28.dist-info/RECORD,,
106
+ halib-0.2.29.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
107
+ halib-0.2.29.dist-info/METADATA,sha256=EtgQqtzHJW2aM8matCUzWUZLpc74XKWj85KeOaIarlw,8174
108
+ halib-0.2.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
109
+ halib-0.2.29.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
110
+ halib-0.2.29.dist-info/RECORD,,
File without changes