halib 0.2.20__tar.gz → 0.2.23__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 (57) hide show
  1. {halib-0.2.20 → halib-0.2.23}/PKG-INFO +6 -2
  2. {halib-0.2.20 → halib-0.2.23}/README.md +5 -1
  3. {halib-0.2.20 → halib-0.2.23}/halib/__init__.py +11 -5
  4. {halib-0.2.20 → halib-0.2.23}/halib/common/common.py +29 -0
  5. {halib-0.2.20 → halib-0.2.23}/halib/exp/core/param_gen.py +19 -38
  6. {halib-0.2.20 → halib-0.2.23}/halib/exp/perf/perfcalc.py +31 -1
  7. halib-0.2.23/halib/utils/dict.py +156 -0
  8. {halib-0.2.20 → halib-0.2.23}/halib.egg-info/PKG-INFO +6 -2
  9. {halib-0.2.20 → halib-0.2.23}/setup.py +1 -1
  10. halib-0.2.20/halib/utils/dict.py +0 -9
  11. {halib-0.2.20 → halib-0.2.23}/.gitignore +0 -0
  12. {halib-0.2.20 → halib-0.2.23}/GDriveFolder.txt +0 -0
  13. {halib-0.2.20 → halib-0.2.23}/LICENSE.txt +0 -0
  14. {halib-0.2.20 → halib-0.2.23}/MANIFEST.in +0 -0
  15. {halib-0.2.20 → halib-0.2.23}/halib/common/__init__.py +0 -0
  16. {halib-0.2.20 → halib-0.2.23}/halib/common/rich_color.py +0 -0
  17. {halib-0.2.20 → halib-0.2.23}/halib/exp/__init__.py +0 -0
  18. {halib-0.2.20 → halib-0.2.23}/halib/exp/core/__init__.py +0 -0
  19. {halib-0.2.20 → halib-0.2.23}/halib/exp/core/base_config.py +0 -0
  20. {halib-0.2.20 → halib-0.2.23}/halib/exp/core/base_exp.py +0 -0
  21. {halib-0.2.20 → halib-0.2.23}/halib/exp/core/wandb_op.py +0 -0
  22. {halib-0.2.20 → halib-0.2.23}/halib/exp/data/__init__.py +0 -0
  23. {halib-0.2.20 → halib-0.2.23}/halib/exp/data/dataclass_util.py +0 -0
  24. {halib-0.2.20 → halib-0.2.23}/halib/exp/data/dataset.py +0 -0
  25. {halib-0.2.20 → halib-0.2.23}/halib/exp/data/torchloader.py +0 -0
  26. {halib-0.2.20 → halib-0.2.23}/halib/exp/perf/__init__.py +0 -0
  27. {halib-0.2.20 → halib-0.2.23}/halib/exp/perf/flop_calc.py +0 -0
  28. {halib-0.2.20 → halib-0.2.23}/halib/exp/perf/gpu_mon.py +0 -0
  29. {halib-0.2.20 → halib-0.2.23}/halib/exp/perf/perfmetrics.py +0 -0
  30. {halib-0.2.20 → halib-0.2.23}/halib/exp/perf/perftb.py +0 -0
  31. {halib-0.2.20 → halib-0.2.23}/halib/exp/perf/profiler.py +0 -0
  32. {halib-0.2.20 → halib-0.2.23}/halib/exp/viz/__init__.py +0 -0
  33. {halib-0.2.20 → halib-0.2.23}/halib/exp/viz/plot.py +0 -0
  34. {halib-0.2.20 → halib-0.2.23}/halib/filetype/__init__.py +0 -0
  35. {halib-0.2.20 → halib-0.2.23}/halib/filetype/csvfile.py +0 -0
  36. {halib-0.2.20 → halib-0.2.23}/halib/filetype/ipynb.py +0 -0
  37. {halib-0.2.20 → halib-0.2.23}/halib/filetype/jsonfile.py +0 -0
  38. {halib-0.2.20 → halib-0.2.23}/halib/filetype/textfile.py +0 -0
  39. {halib-0.2.20 → halib-0.2.23}/halib/filetype/videofile.py +0 -0
  40. {halib-0.2.20 → halib-0.2.23}/halib/filetype/yamlfile.py +0 -0
  41. {halib-0.2.20 → halib-0.2.23}/halib/online/__init__.py +0 -0
  42. {halib-0.2.20 → halib-0.2.23}/halib/online/gdrive.py +0 -0
  43. {halib-0.2.20 → halib-0.2.23}/halib/online/gdrive_mkdir.py +0 -0
  44. {halib-0.2.20 → halib-0.2.23}/halib/online/projectmake.py +0 -0
  45. {halib-0.2.20 → halib-0.2.23}/halib/online/tele_noti.py +0 -0
  46. {halib-0.2.20 → halib-0.2.23}/halib/system/__init__.py +0 -0
  47. {halib-0.2.20 → halib-0.2.23}/halib/system/_list_pc.csv +0 -0
  48. {halib-0.2.20 → halib-0.2.23}/halib/system/cmd.py +0 -0
  49. {halib-0.2.20 → halib-0.2.23}/halib/system/filesys.py +0 -0
  50. {halib-0.2.20 → halib-0.2.23}/halib/system/path.py +0 -0
  51. {halib-0.2.20 → halib-0.2.23}/halib/utils/__init__.py +0 -0
  52. {halib-0.2.20 → halib-0.2.23}/halib/utils/list.py +0 -0
  53. {halib-0.2.20 → halib-0.2.23}/halib.egg-info/SOURCES.txt +0 -0
  54. {halib-0.2.20 → halib-0.2.23}/halib.egg-info/dependency_links.txt +0 -0
  55. {halib-0.2.20 → halib-0.2.23}/halib.egg-info/requires.txt +0 -0
  56. {halib-0.2.20 → halib-0.2.23}/halib.egg-info/top_level.txt +0 -0
  57. {halib-0.2.20 → halib-0.2.23}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.20
3
+ Version: 0.2.23
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -56,7 +56,11 @@ Dynamic: summary
56
56
 
57
57
  ## v0.2.x (Experiment & Core Updates)
58
58
 
59
- ### **v0.2.20**
59
+ ### **v0.2.23**
60
+ - ✨ **New Feature:** Added `utils.dict.DictUtils` for advanced dictionary manipulations (merging, filtering, transforming).
61
+
62
+ - ✨ **New Feature:** Added `common.common.pprint_stack_trace` to print stack traces with optional custom messages and force stop capability.
63
+
60
64
  - 🚀 **Improvement:** `exp.perf.profiler` - allow to export *report dict* as csv files for further analysis
61
65
 
62
66
  ### **v0.2.19**
@@ -2,7 +2,11 @@
2
2
 
3
3
  ## v0.2.x (Experiment & Core Updates)
4
4
 
5
- ### **v0.2.20**
5
+ ### **v0.2.23**
6
+ - ✨ **New Feature:** Added `utils.dict.DictUtils` for advanced dictionary manipulations (merging, filtering, transforming).
7
+
8
+ - ✨ **New Feature:** Added `common.common.pprint_stack_trace` to print stack traces with optional custom messages and force stop capability.
9
+
6
10
  - 🚀 **Improvement:** `exp.perf.profiler` - allow to export *report dict* as csv files for further analysis
7
11
 
8
12
  ### **v0.2.19**
@@ -19,11 +19,11 @@ __all__ = [
19
19
  "os",
20
20
  "pd",
21
21
  "plt",
22
- "pprint",
23
22
  "pprint_box",
24
23
  "pprint_local_path",
24
+ "pprint_stack_trace",
25
+ "pprint",
25
26
  "px",
26
- "pprint_local_path",
27
27
  "rcolor_all_str",
28
28
  "rcolor_palette_all",
29
29
  "rcolor_palette",
@@ -32,10 +32,10 @@ __all__ = [
32
32
  "rprint",
33
33
  "sns",
34
34
  "tcuda",
35
+ "time",
35
36
  "timebudget",
36
37
  "tqdm",
37
38
  "warnings",
38
- "time",
39
39
  ]
40
40
  import warnings
41
41
 
@@ -64,7 +64,8 @@ from .common.common import (
64
64
  norm_str,
65
65
  pprint_box,
66
66
  pprint_local_path,
67
- tcuda
67
+ pprint_stack_trace,
68
+ tcuda,
68
69
  )
69
70
 
70
71
  # for log
@@ -76,7 +77,12 @@ from timebudget import timebudget
76
77
  import omegaconf
77
78
  from omegaconf import OmegaConf
78
79
  from omegaconf.dictconfig import DictConfig
79
- from .common.rich_color import rcolor_str, rcolor_palette, rcolor_palette_all, rcolor_all_str
80
+ from .common.rich_color import (
81
+ rcolor_str,
82
+ rcolor_palette,
83
+ rcolor_palette_all,
84
+ rcolor_all_str,
85
+ )
80
86
 
81
87
  # for visualization
82
88
  import seaborn as sns
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import sys
2
3
  import re
3
4
  import arrow
4
5
  import importlib
@@ -10,6 +11,8 @@ from rich.console import Console
10
11
  from rich.pretty import pprint, Pretty
11
12
 
12
13
  from pathlib import Path, PureWindowsPath
14
+ from typing import Optional
15
+ from loguru import logger
13
16
 
14
17
 
15
18
  console = Console()
@@ -114,6 +117,32 @@ def linux_to_wins_path(path: str) -> str:
114
117
  return str(PureWindowsPath(win_path))
115
118
 
116
119
 
120
+ DEFAULT_STACK_TRACE_MSG = "Caused by halib.common.common.pprint_stack_trace"
121
+
122
+
123
+ def pprint_stack_trace(
124
+ msg: str = DEFAULT_STACK_TRACE_MSG,
125
+ e: Optional[Exception] = None,
126
+ force_stop: bool = False,
127
+ ):
128
+ """
129
+ Print the current stack trace or the stack trace of an exception.
130
+ """
131
+ try:
132
+ if e is not None:
133
+ raise e
134
+ else:
135
+ raise Exception("pprint_stack_trace called")
136
+ except Exception as e:
137
+ # attach the exception trace to a warning
138
+ global DEFAULT_STACK_TRACE_MSG
139
+ if len(msg.strip()) == 0:
140
+ msg = DEFAULT_STACK_TRACE_MSG
141
+ logger.opt(exception=e).warning(msg)
142
+ if force_stop:
143
+ console.rule("[red]Force Stop Triggered in <halib.common.pprint_stack_trace>[/red]")
144
+ sys.exit(1)
145
+
117
146
  def pprint_local_path(
118
147
  local_path: str, get_wins_path: bool = False, tag: str = ""
119
148
  ) -> str:
@@ -4,6 +4,11 @@ import numpy as np
4
4
  from itertools import product
5
5
  from typing import Dict, Any, List, Iterator, Optional
6
6
  from ...filetype import yamlfile
7
+ from ...utils.dict import DictUtils
8
+
9
+ # Assuming DictUtils is available in the scope or imported
10
+ # from .dict_utils import DictUtils
11
+
7
12
 
8
13
  class ParamGen:
9
14
  """
@@ -38,6 +43,7 @@ class ParamGen:
38
43
  keys (List[str]): List of flattened dot-notation keys being swept.
39
44
  values (List[List[Any]]): List of value options for each key.
40
45
  """
46
+
41
47
  def __init__(
42
48
  self, sweep_cfg: Dict[str, Any], base_cfg: Optional[Dict[str, Any]] = None
43
49
  ):
@@ -50,7 +56,12 @@ class ParamGen:
50
56
  self.base_cfg = base_cfg if base_cfg is not None else {}
51
57
 
52
58
  # Recursively flatten the nested sweep config into dot-notation keys
53
- self.param_space = self._flatten_params(sweep_cfg)
59
+ # Refactored to use DictUtils, passing our custom leaf logic
60
+ flat_sweep = DictUtils.flatten(sweep_cfg, is_leaf_predicate=self._is_sweep_leaf)
61
+
62
+ # Expand values (ranges, strings) which DictUtils leaves as-is
63
+ self.param_space = {k: self._expand_val(v) for k, v in flat_sweep.items()}
64
+
54
65
  self.keys = list(self.param_space.keys())
55
66
  self.values = list(self.param_space.values())
56
67
 
@@ -66,13 +77,16 @@ class ParamGen:
66
77
 
67
78
  # 2. Deep copy base and update with current params
68
79
  new_cfg = copy.deepcopy(self.base_cfg)
69
- new_cfg = self._apply_updates(new_cfg, flat_params)
80
+
81
+ # Refactored: Unflatten the specific params, then deep merge
82
+ update_structure = DictUtils.unflatten(flat_params)
83
+ DictUtils.deep_update(new_cfg, update_structure)
70
84
 
71
85
  # 3. Store metadata (Optional)
72
86
  # if "_meta" not in new_cfg:
73
87
  # new_cfg["_meta"] = {}
74
88
  # We unflatten the sweep params here so the log is readable
75
- # new_cfg["_meta"]["sweep_params"] = self._unflatten(flat_params)
89
+ # new_cfg["_meta"]["sweep_params"] = DictUtils.unflatten(flat_params)
76
90
 
77
91
  yield new_cfg
78
92
 
@@ -124,27 +138,8 @@ class ParamGen:
124
138
  combinations.append(flat_dict)
125
139
  return combinations
126
140
 
127
- def _unflatten(self, flat_dict: Dict[str, Any]) -> Dict[str, Any]:
128
- """Converts {'a.b': 1} back to {'a': {'b': 1}}."""
129
- nested = {}
130
- self._apply_updates(nested, flat_dict)
131
- return nested
132
-
133
- def _flatten_params(
134
- self, cfg: Dict[str, Any], parent_key: str = ""
135
- ) -> Dict[str, List[Any]]:
136
- """Recursively converts nested dicts into flat dot-notation keys."""
137
- flat = {}
138
- for key, val in cfg.items():
139
- current_key = f"{parent_key}.{key}" if parent_key else key
140
-
141
- if self._is_sweep_leaf(val):
142
- flat[current_key] = self._expand_val(val)
143
- elif isinstance(val, dict):
144
- flat.update(self._flatten_params(val, current_key))
145
- else:
146
- flat[current_key] = [val]
147
- return flat
141
+ # Note: _unflatten, _flatten_params, and _apply_updates have been removed
142
+ # as they are replaced by DictUtils methods.
148
143
 
149
144
  def _is_sweep_leaf(self, val: Any) -> bool:
150
145
  if isinstance(val, list):
@@ -173,17 +168,3 @@ class ParamGen:
173
168
  return np.arange(val["start"], val["stop"], step).tolist()
174
169
 
175
170
  return [val]
176
-
177
- def _apply_updates(
178
- self, cfg: Dict[str, Any], updates: Dict[str, Any]
179
- ) -> Dict[str, Any]:
180
- """Deep merges dot-notation updates into cfg."""
181
- for key, val in updates.items():
182
- parts = key.split(".")
183
- target = cfg
184
- for part in parts[:-1]:
185
- if part not in target:
186
- target[part] = {}
187
- target = target[part]
188
- target[parts[-1]] = val
189
- return cfg
@@ -210,7 +210,37 @@ class PerfCalc(ABC): # Abstract base class for performance calculation
210
210
  **kwargs,
211
211
  ) -> Tuple[Union[List[OrderedDict], pd.DataFrame], Optional[str]]:
212
212
  """
213
- Standard use case: Calculate metrics AND save to CSV.
213
+ Orchestrates the calculation of performance metrics and saves the results to a CSV.
214
+
215
+ This method acts as a pipeline:
216
+ 1. Calculates metrics (Accuracy, F1, etc.) using `raw_metrics_data`.
217
+ 2. Merges in `extra_data` (metadata) to the results.
218
+ 3. Saves the combined data to a CSV file.
219
+
220
+ Args:
221
+ raw_metrics_data (Union[List[dict], dict]):
222
+ The input data required for metric calculations.
223
+ Structure: A dictionary where keys are metric names (e.g., 'accuracy')
224
+ and values are inputs (e.g., {'preds': ..., 'target': ...}).
225
+
226
+ extra_data (Optional[Union[List[dict], dict]]):
227
+ Static metadata or context to attach to the result rows.
228
+ These fields are NOT used for calculation but are passed through
229
+ to the final output (DataFrame/CSV).
230
+
231
+ Example:
232
+ If extra_data={'model_version': 'v1.0', 'epoch': 5},
233
+ the output CSV will include columns 'model_version' and 'epoch'.
234
+
235
+ *args, **kwargs:
236
+ Additional arguments passed to `calc_exp_perf_metrics` or `save_results_to_csv`
237
+ (e.g., `outfile`, `return_df`).
238
+
239
+ Returns:
240
+ Tuple[Union[List[OrderedDict], pd.DataFrame], Optional[str]]:
241
+ A tuple containing:
242
+ 1. The results as a DataFrame (if return_df=True) or a list of dicts.
243
+ 2. The path to the saved output file (or None if save failed).
214
244
  """
215
245
  metric_names = self.get_metric_backend().metric_names
216
246
 
@@ -0,0 +1,156 @@
1
+ from typing import Dict, Any, Callable, Optional
2
+ from rich.pretty import pprint
3
+
4
+ class DictUtils:
5
+ """
6
+ General-purpose dictionary manipulation utilities.
7
+ """
8
+
9
+ @staticmethod
10
+ def flatten(
11
+ d: Dict[str, Any],
12
+ parent_key: str = "",
13
+ sep: str = ".",
14
+ is_leaf_predicate: Optional[Callable[[Any], bool]] = None,
15
+ ) -> Dict[str, Any]:
16
+ """
17
+ Recursively flattens a nested dictionary.
18
+
19
+ Args:
20
+ d: The dictionary to flatten.
21
+ parent_key: Prefix for keys (used during recursion).
22
+ sep: Separator for dot-notation keys.
23
+ is_leaf_predicate: Optional function that returns True if a value should
24
+ be treated as a leaf (value) rather than a branch to recurse.
25
+ Useful if you have dicts you don't want flattened.
26
+ """
27
+ items = []
28
+ for k, v in d.items():
29
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
30
+
31
+ # Check if we should treat this as a leaf (custom logic)
32
+ if is_leaf_predicate and is_leaf_predicate(v):
33
+ items.append((new_key, v))
34
+ # Standard recursion
35
+ elif isinstance(v, dict):
36
+ items.extend(
37
+ DictUtils.flatten(
38
+ v, new_key, sep=sep, is_leaf_predicate=is_leaf_predicate
39
+ ).items()
40
+ )
41
+ else:
42
+ items.append((new_key, v))
43
+ return dict(items)
44
+
45
+ @staticmethod
46
+ def unflatten(flat_dict: Dict[str, Any], sep: str = ".") -> Dict[str, Any]:
47
+ """
48
+ Converts flat dot-notation keys back to nested dictionaries.
49
+ e.g., {'a.b': 1} -> {'a': {'b': 1}}
50
+ """
51
+ nested = {}
52
+ for key, value in flat_dict.items():
53
+ DictUtils.deep_set(nested, key, value, sep=sep)
54
+ return nested
55
+
56
+ @staticmethod
57
+ def deep_update(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
58
+ """
59
+ Recursively merges 'update' dict into 'base' dict.
60
+
61
+ Unlike the standard `dict.update()`, which replaces nested dictionaries entirely,
62
+ this method enters nested dictionaries and updates them key-by-key. This preserves
63
+ existing keys in 'base' that are not present in 'update'.
64
+
65
+ Args:
66
+ base: The original dictionary to modify.
67
+ update: The dictionary containing new values.
68
+
69
+ Returns:
70
+ The modified 'base' dictionary.
71
+
72
+ Example:
73
+ >>> base = {'model': {'name': 'v1', 'dropout': 0.5}}
74
+ >>> new_vals = {'model': {'name': 'v2'}}
75
+ >>> # Standard update would delete 'dropout'. deep_update keeps it:
76
+ >>> DictUtils.deep_update(base, new_vals)
77
+ {'model': {'name': 'v2', 'dropout': 0.5}}
78
+ """
79
+ for k, v in update.items():
80
+ if isinstance(v, dict) and k in base and isinstance(base[k], dict):
81
+ DictUtils.deep_update(base[k], v)
82
+ else:
83
+ base[k] = v
84
+ return base
85
+
86
+ @staticmethod
87
+ def deep_set(d: Dict[str, Any], dot_key: str, value: Any, sep: str = ".") -> None:
88
+ """
89
+ Sets a value in a nested dictionary using a dot-notation key path.
90
+ Automatically creates any missing intermediate dictionaries.
91
+
92
+ Args:
93
+ d: The dictionary to modify.
94
+ dot_key: The path to the value (e.g., "model.backbone.layers").
95
+ value: The value to set.
96
+ sep: The separator used in the key (default is ".").
97
+
98
+ Example:
99
+ >>> cfg = {}
100
+ >>> DictUtils.deep_set(cfg, "a.b.c", 10)
101
+ >>> print(cfg)
102
+ {'a': {'b': {'c': 10}}}
103
+ """
104
+ parts = dot_key.split(sep)
105
+ target = d
106
+ for part in parts[:-1]:
107
+ if part not in target:
108
+ target[part] = {}
109
+ target = target[part]
110
+ if not isinstance(target, dict):
111
+ # Handle conflict if a path was previously a value (e.g. overwriting a leaf)
112
+ target = {}
113
+ target[parts[-1]] = value
114
+
115
+
116
+ if __name__ == "__main__":
117
+ # --- Setup ---
118
+ base_config = {
119
+ "model": {
120
+ "name": "ResNet50",
121
+ "layers": 50,
122
+ "details": {
123
+ "activation": "relu",
124
+ "dropout": 0.5, # <--- We want to keep this
125
+ },
126
+ },
127
+ "epochs": 10,
128
+ }
129
+
130
+ new_settings = {
131
+ "model": {"details": {"activation": "gelu"}} # <--- We only want to change this
132
+ }
133
+
134
+ b1 = base_config.copy()
135
+ b2 = base_config.copy()
136
+ n1 = new_settings.copy()
137
+ n2 = new_settings.copy()
138
+
139
+ pprint("Base Config:")
140
+ pprint(base_config)
141
+ pprint("New Settings:")
142
+ pprint(new_settings)
143
+ print("*" * 40)
144
+ pprint(
145
+ "Task: Update base_config with new_settings, preserving unspecified nested keys."
146
+ )
147
+ print("*" * 40)
148
+
149
+ # --- Standard Update (The Problem) ---
150
+ pprint("Normal Update Result:")
151
+ b1.update(n1)
152
+ pprint(b1) # type: ignore[return-value]
153
+
154
+ # --- Deep Update (The Solution) ---
155
+ pprint("Deep Update Result:")
156
+ pprint(DictUtils.deep_update(b2, n2))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.20
3
+ Version: 0.2.23
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -56,7 +56,11 @@ Dynamic: summary
56
56
 
57
57
  ## v0.2.x (Experiment & Core Updates)
58
58
 
59
- ### **v0.2.20**
59
+ ### **v0.2.23**
60
+ - ✨ **New Feature:** Added `utils.dict.DictUtils` for advanced dictionary manipulations (merging, filtering, transforming).
61
+
62
+ - ✨ **New Feature:** Added `common.common.pprint_stack_trace` to print stack traces with optional custom messages and force stop capability.
63
+
60
64
  - 🚀 **Improvement:** `exp.perf.profiler` - allow to export *report dict* as csv files for further analysis
61
65
 
62
66
  ### **v0.2.19**
@@ -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.20",
11
+ version="0.2.23",
12
12
  author="Hoang Van Ha",
13
13
  author_email="hoangvanhauit@gmail.com",
14
14
  description="Small library for common tasks",
@@ -1,9 +0,0 @@
1
- def flatten_dict(d, parent_key="", sep="."):
2
- items = {}
3
- for k, v in d.items():
4
- key = f"{parent_key}{sep}{k}" if parent_key else k
5
- if isinstance(v, dict):
6
- items.update(flatten_dict(v, key, sep=sep))
7
- else:
8
- items[key] = v
9
- return items
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