halib 0.2.28__tar.gz → 0.2.29__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.
- {halib-0.2.28 → halib-0.2.29}/PKG-INFO +7 -1
- {halib-0.2.28 → halib-0.2.29}/README.md +6 -0
- {halib-0.2.28 → halib-0.2.29}/halib/__init__.py +4 -0
- {halib-0.2.28 → halib-0.2.29}/halib/common/common.py +122 -3
- {halib-0.2.28 → halib-0.2.29}/halib/utils/dict.py +95 -68
- {halib-0.2.28 → halib-0.2.29}/halib/utils/slack.py +3 -1
- {halib-0.2.28 → halib-0.2.29}/halib.egg-info/PKG-INFO +7 -1
- {halib-0.2.28 → halib-0.2.29}/setup.py +1 -1
- {halib-0.2.28 → halib-0.2.29}/.gitignore +0 -0
- {halib-0.2.28 → halib-0.2.29}/GDriveFolder.txt +0 -0
- {halib-0.2.28 → halib-0.2.29}/LICENSE.txt +0 -0
- {halib-0.2.28 → halib-0.2.29}/MANIFEST.in +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/common/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/common/rich_color.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/core/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/core/base_config.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/core/base_exp.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/core/param_gen.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/core/wandb_op.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/data/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/data/dataclass_util.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/data/dataset.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/data/torchloader.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/perf/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/perf/flop_calc.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/perf/gpu_mon.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/perf/perfcalc.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/perf/perfmetrics.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/perf/perftb.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/perf/profiler.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/viz/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/exp/viz/plot.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/filetype/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/filetype/csvfile.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/filetype/ipynb.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/filetype/jsonfile.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/filetype/textfile.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/filetype/videofile.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/filetype/yamlfile.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/online/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/online/gdrive.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/online/gdrive_mkdir.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/online/projectmake.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/online/tele_noti.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/system/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/system/_list_pc.csv +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/system/cmd.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/system/filesys.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/system/path.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/utils/__init__.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib/utils/list.py +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib.egg-info/SOURCES.txt +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib.egg-info/dependency_links.txt +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib.egg-info/requires.txt +0 -0
- {halib-0.2.28 → halib-0.2.29}/halib.egg-info/top_level.txt +0 -0
- {halib-0.2.28 → halib-0.2.29}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: halib
|
|
3
|
-
Version: 0.2.
|
|
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
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## v0.2.x (Experiment & Core Updates)
|
|
4
4
|
|
|
5
|
+
### **v0.2.29**
|
|
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_remove` 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(
|
|
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")))
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: halib
|
|
3
|
-
Version: 0.2.
|
|
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
|
|
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
|
|
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
|
|
File without changes
|