halib 0.2.32__tar.gz → 0.2.34__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 (60) hide show
  1. {halib-0.2.32 → halib-0.2.34}/.gitignore +6 -1
  2. halib-0.2.34/MANIFEST.in +6 -0
  3. {halib-0.2.32 → halib-0.2.34}/PKG-INFO +7 -2
  4. {halib-0.2.32 → halib-0.2.34}/README.md +6 -1
  5. halib-0.2.34/halib/utils/plotly_op.py +234 -0
  6. halib-0.2.34/halib/utils/wandb_op.py +140 -0
  7. {halib-0.2.32 → halib-0.2.34}/halib.egg-info/PKG-INFO +7 -2
  8. {halib-0.2.32 → halib-0.2.34}/halib.egg-info/SOURCES.txt +3 -2
  9. {halib-0.2.32 → halib-0.2.34}/setup.py +1 -1
  10. halib-0.2.32/MANIFEST.in +0 -5
  11. halib-0.2.32/halib/exp/core/wandb_op.py +0 -117
  12. {halib-0.2.32 → halib-0.2.34}/GDriveFolder.txt +0 -0
  13. {halib-0.2.32 → halib-0.2.34}/LICENSE.txt +0 -0
  14. {halib-0.2.32 → halib-0.2.34}/halib/__init__.py +0 -0
  15. {halib-0.2.32 → halib-0.2.34}/halib/common/__init__.py +0 -0
  16. {halib-0.2.32 → halib-0.2.34}/halib/common/common.py +0 -0
  17. {halib-0.2.32 → halib-0.2.34}/halib/common/rich_color.py +0 -0
  18. {halib-0.2.32 → halib-0.2.34}/halib/exp/__init__.py +0 -0
  19. {halib-0.2.32 → halib-0.2.34}/halib/exp/core/__init__.py +0 -0
  20. {halib-0.2.32 → halib-0.2.34}/halib/exp/core/base_config.py +0 -0
  21. {halib-0.2.32 → halib-0.2.34}/halib/exp/core/base_exp.py +0 -0
  22. {halib-0.2.32 → halib-0.2.34}/halib/exp/core/param_gen.py +0 -0
  23. {halib-0.2.32 → halib-0.2.34}/halib/exp/data/__init__.py +0 -0
  24. {halib-0.2.32 → halib-0.2.34}/halib/exp/data/dataclass_util.py +0 -0
  25. {halib-0.2.32 → halib-0.2.34}/halib/exp/data/dataset.py +0 -0
  26. {halib-0.2.32 → halib-0.2.34}/halib/exp/data/torchloader.py +0 -0
  27. {halib-0.2.32 → halib-0.2.34}/halib/exp/perf/__init__.py +0 -0
  28. {halib-0.2.32 → halib-0.2.34}/halib/exp/perf/flop_calc.py +0 -0
  29. {halib-0.2.32 → halib-0.2.34}/halib/exp/perf/gpu_mon.py +0 -0
  30. {halib-0.2.32 → halib-0.2.34}/halib/exp/perf/perfcalc.py +0 -0
  31. {halib-0.2.32 → halib-0.2.34}/halib/exp/perf/perfmetrics.py +0 -0
  32. {halib-0.2.32 → halib-0.2.34}/halib/exp/perf/perftb.py +0 -0
  33. {halib-0.2.32 → halib-0.2.34}/halib/exp/perf/profiler.py +0 -0
  34. {halib-0.2.32 → halib-0.2.34}/halib/exp/viz/__init__.py +0 -0
  35. {halib-0.2.32 → halib-0.2.34}/halib/exp/viz/plot.py +0 -0
  36. {halib-0.2.32 → halib-0.2.34}/halib/filetype/__init__.py +0 -0
  37. {halib-0.2.32 → halib-0.2.34}/halib/filetype/csvfile.py +0 -0
  38. {halib-0.2.32 → halib-0.2.34}/halib/filetype/ipynb.py +0 -0
  39. {halib-0.2.32 → halib-0.2.34}/halib/filetype/jsonfile.py +0 -0
  40. {halib-0.2.32 → halib-0.2.34}/halib/filetype/textfile.py +0 -0
  41. {halib-0.2.32 → halib-0.2.34}/halib/filetype/videofile.py +0 -0
  42. {halib-0.2.32 → halib-0.2.34}/halib/filetype/yamlfile.py +0 -0
  43. {halib-0.2.32 → halib-0.2.34}/halib/online/__init__.py +0 -0
  44. {halib-0.2.32 → halib-0.2.34}/halib/online/gdrive.py +0 -0
  45. {halib-0.2.32 → halib-0.2.34}/halib/online/gdrive_mkdir.py +0 -0
  46. {halib-0.2.32 → halib-0.2.34}/halib/online/projectmake.py +0 -0
  47. {halib-0.2.32 → halib-0.2.34}/halib/online/tele_noti.py +0 -0
  48. {halib-0.2.32 → halib-0.2.34}/halib/system/__init__.py +0 -0
  49. {halib-0.2.32 → halib-0.2.34}/halib/system/_list_pc.csv +0 -0
  50. {halib-0.2.32 → halib-0.2.34}/halib/system/cmd.py +0 -0
  51. {halib-0.2.32 → halib-0.2.34}/halib/system/filesys.py +0 -0
  52. {halib-0.2.32 → halib-0.2.34}/halib/system/path.py +0 -0
  53. {halib-0.2.32 → halib-0.2.34}/halib/utils/__init__.py +0 -0
  54. {halib-0.2.32 → halib-0.2.34}/halib/utils/dict.py +0 -0
  55. {halib-0.2.32 → halib-0.2.34}/halib/utils/list.py +0 -0
  56. /halib-0.2.32/halib/utils/slack.py → /halib-0.2.34/halib/utils/slack_op.py +0 -0
  57. {halib-0.2.32 → halib-0.2.34}/halib.egg-info/dependency_links.txt +0 -0
  58. {halib-0.2.32 → halib-0.2.34}/halib.egg-info/requires.txt +0 -0
  59. {halib-0.2.32 → halib-0.2.34}/halib.egg-info/top_level.txt +0 -0
  60. {halib-0.2.32 → halib-0.2.34}/setup.cfg +0 -0
@@ -53,4 +53,9 @@ dist
53
53
 
54
54
  venv*/
55
55
 
56
- test/test_plot/output
56
+ test/test_plot/output
57
+
58
+ # api keys and configurations
59
+ .env.yaml
60
+
61
+ test/test_utils/test_results_with_table.html
@@ -0,0 +1,6 @@
1
+ prune _archived
2
+ prune .env.yaml
3
+ prune test
4
+ prune zout
5
+ exclude *.toml
6
+ include halib/system/_list_pc.csv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.32
3
+ Version: 0.2.34
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -57,7 +57,12 @@ Dynamic: summary
57
57
 
58
58
  ## v0.2.x (Experiment & Core Updates)
59
59
 
60
- ### **v0.2.32**
60
+ ### **v0.2.34**
61
+
62
+ - ✨ **New Feature:**: introduce `utils.PlotlyUtils` with parallel coordinates plot and data table support
63
+
64
+ - 🚀 **Improvement:**: move `wandb_op.py` to `utils` and add `scripts` folder
65
+
61
66
 
62
67
  - ✨ **New Feature:**: add `common.common.log_func` as decorator to log function entry, exit, with execution time and arguments.
63
68
 
@@ -2,7 +2,12 @@
2
2
 
3
3
  ## v0.2.x (Experiment & Core Updates)
4
4
 
5
- ### **v0.2.32**
5
+ ### **v0.2.34**
6
+
7
+ - ✨ **New Feature:**: introduce `utils.PlotlyUtils` with parallel coordinates plot and data table support
8
+
9
+ - 🚀 **Improvement:**: move `wandb_op.py` to `utils` and add `scripts` folder
10
+
6
11
 
7
12
  - ✨ **New Feature:**: add `common.common.log_func` as decorator to log function entry, exit, with execution time and arguments.
8
13
 
@@ -0,0 +1,234 @@
1
+ import os
2
+ import pandas as pd
3
+ from typing import List, Optional, Callable, Union
4
+ from rich.pretty import pprint
5
+ import plotly.express as px
6
+ from ..common.common import pprint_local_path
7
+
8
+
9
+ class PlotlyUtils:
10
+ @staticmethod
11
+ # Extract experiment IDs from complex naming convention
12
+ def exp_id_extractor(df: pd.DataFrame) -> pd.Series:
13
+ # MainPC__ds_UFireIndoor2__mt_temp_method_motion_block__9896ed2e67e7__20260202.133758
14
+ exp_names = df["Name"].to_list()
15
+ exp_ids = []
16
+ for name in exp_names:
17
+ parts = name.split("__")
18
+ exp_id = parts[-2] if len(parts) >= 2 else name
19
+ exp_ids.append(exp_id)
20
+ return pd.Series(exp_ids)
21
+
22
+ @staticmethod
23
+ def exp_id_formatter(exp_id: str) -> str:
24
+ if len(exp_id) <= 6:
25
+ return exp_id
26
+ return f"{exp_id[:6]}"
27
+
28
+ @staticmethod
29
+ def parallel_plot(
30
+ df_or_csv_file: Union[pd.DataFrame, str],
31
+ dimensions: List[str] = [],
32
+ exclude_dims: List[str] = [],
33
+ exp_col_or_func: Union[
34
+ str, Callable[[pd.DataFrame], pd.Series]
35
+ ] = exp_id_extractor,
36
+ exp_id_formatter: Optional[Callable[[str], str]] = exp_id_formatter,
37
+ color: Optional[str] = None,
38
+ csv_separator: str = ";",
39
+ plot_bar_height: int = 1200,
40
+ plot_width: int = 1500,
41
+ title: str = "Parallel Coordinates Plot",
42
+ template: str = "plotly_white",
43
+ outdir: str = ".",
44
+ outfile: str = "zresults_with_table.html",
45
+ ):
46
+ # 1. Unified Data Loading
47
+ if isinstance(df_or_csv_file, str):
48
+ df = pd.read_csv(df_or_csv_file, sep=csv_separator, encoding="utf-8")
49
+ else:
50
+ df = df_or_csv_file.copy()
51
+
52
+ # 2. Extract Experiment IDs
53
+ if callable(exp_col_or_func):
54
+ df["exp_id"] = exp_col_or_func(df)
55
+ elif isinstance(exp_col_or_func, str):
56
+ if exp_col_or_func not in df.columns:
57
+ raise ValueError(f"Column '{exp_col_or_func}' not found.")
58
+ df["exp_id"] = df[exp_col_or_func].copy()
59
+ exclude_dims.append(exp_col_or_func)
60
+ else:
61
+ raise ValueError("exp_col_or_func must be a column name or a callable.")
62
+
63
+ # 3. Setup Plot Dimensions
64
+ # Priority: explicit dimensions -> all columns minus exclusions
65
+ if not dimensions:
66
+ dimensions = [
67
+ c for c in df.columns if c not in exclude_dims and c != "exp_id"
68
+ ]
69
+
70
+ df["numeric_id"] = range(len(df))
71
+ # Ensure numeric_id is the first axis for the labels to work
72
+ final_plot_dims = ["numeric_id"] + [
73
+ d for d in dimensions if d != "numeric_id" and d in df.columns
74
+ ]
75
+
76
+ pprint(f"Generating plot: {title}")
77
+
78
+ # 4. Create and Configure Plotly Figure
79
+ fig = px.parallel_coordinates(
80
+ df,
81
+ dimensions=final_plot_dims,
82
+ color=color,
83
+ title=title,
84
+ template=template,
85
+ )
86
+
87
+ # Map string IDs to the numeric axis
88
+ fig.data[0].dimensions[0].tickvals = list(df["numeric_id"])
89
+ fig.data[0].dimensions[0].ticktext = (
90
+ [exp_id_formatter(i) for i in df["exp_id"]]
91
+ if exp_id_formatter
92
+ else df["exp_id"]
93
+ )
94
+ fig.data[0].dimensions[0].label = "Exp IDs"
95
+
96
+ fig.update_layout(
97
+ title={
98
+ "text": title,
99
+ "y": 0.98,
100
+ "x": 0.5,
101
+ "xanchor": "center",
102
+ "yanchor": "top",
103
+ },
104
+ width=plot_width,
105
+ height=plot_bar_height,
106
+ margin=dict(l=150, r=50, t=150, b=50),
107
+ )
108
+
109
+ # 5. Prepare Table Display
110
+ # Clean up columns: Move exp_id to front, drop internal numeric_id
111
+ cols = ["exp_id"] + [c for c in df.columns if c not in ["exp_id", "numeric_id"]]
112
+ df_display = df[cols].copy()
113
+ df_display.insert(0, "Selection", '<button class="select-btn">Select</button>')
114
+
115
+ chart_html = fig.to_html(full_html=False, include_plotlyjs="cdn")
116
+
117
+ # Create a display copy with a 'Select' button column
118
+ df_display = df.copy()
119
+ df_display_cols = df_display.columns.tolist()
120
+ # move 'exp_id' to the front for better visibility
121
+ if "exp_id" in df_display_cols:
122
+ df_display_cols.insert(
123
+ 0, df_display_cols.pop(df_display_cols.index("exp_id"))
124
+ )
125
+ df_display = df_display[df_display_cols]
126
+ # remove 'numeric_id' from display
127
+ if "numeric_id" in df_display.columns:
128
+ df_display = df_display.drop(columns=["numeric_id"])
129
+ df_display.insert(0, "Selection", '<button class="select-btn">Select</button>')
130
+
131
+ table_html = df_display.to_html(
132
+ classes="display nowrap", table_id="exp_table", index=False, escape=False
133
+ )
134
+
135
+ final_html = f"""
136
+ <!DOCTYPE html>
137
+ <html>
138
+ <head>
139
+ <title>Experiment Dashboard</title>
140
+ <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.css">
141
+ <script type="text/javascript" charset="utf8" src="https://code.jquery.com/jquery-3.5.1.js"></script>
142
+ <script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.js"></script>
143
+ <style>
144
+ body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; background-color: #f0f2f5; }}
145
+ .container {{ background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }}
146
+ #exp_table tbody tr:nth-child(even), #selected_table tbody tr:nth-child(even) {{ background-color: #f2f2f2; }}
147
+ #exp_table tbody tr:nth-child(odd), #selected_table tbody tr:nth-child(odd) {{ background-color: #ffffff; }}
148
+ #exp_table tbody tr:hover, #selected_table tbody tr:hover {{ background-color: #e0e0e0; }}
149
+ table.dataTable thead th {{ background-color: #333; color: white; padding: 12px; }}
150
+ h2 {{ color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-top: 40px; }}
151
+ .select-btn {{ background-color: #28a745; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 4px; }}
152
+ .remove-btn {{ background-color: #dc3545; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 4px; }}
153
+ .clear-btn {{ background-color: #6c757d; color: white; border: none; padding: 10px 15px; cursor: pointer; border-radius: 4px; margin-bottom: 10px; }}
154
+ </style>
155
+ </head>
156
+ <body>
157
+ <div class="container">
158
+ {chart_html}
159
+
160
+ <h2>Selected Experiments</h2>
161
+ <button id="clear_all" class="clear-btn">Clear All</button>
162
+ <div style="overflow-x:auto; margin-bottom: 40px;">
163
+ <table id="selected_table" class="display nowrap">
164
+ <thead>{df_display.iloc[:0].to_html(index=False, escape=False).split("<thead>")[1].split("</thead>")[0]}</thead>
165
+ <tbody></tbody>
166
+ </table>
167
+ </div>
168
+
169
+ <hr>
170
+
171
+ <h2>Full Experiment Raw Data</h2>
172
+ <div style="overflow-x:auto;">
173
+ {table_html}
174
+ </div>
175
+ </div>
176
+
177
+ <script>
178
+ $(document).ready( function () {{
179
+ var mainTable = $('#exp_table').DataTable({{
180
+ "pageLength": 25,
181
+ "order": [[ 5, "desc" ]],
182
+ "stripeClasses": []
183
+ }});
184
+
185
+ var selectedTable = $('#selected_table').DataTable({{
186
+ "paging": false,
187
+ "searching": false,
188
+ "info": false,
189
+ "stripeClasses": []
190
+ }});
191
+
192
+ // Handle Select button
193
+ $('#exp_table tbody').on('click', '.select-btn', function () {{
194
+ var data = mainTable.row($(this).parents('tr')).data();
195
+ var rowNode = $(this).parents('tr');
196
+
197
+ // Change button to remove in the new table
198
+ var newData = [...data];
199
+ newData[0] = '<button class="remove-btn">Remove</button>';
200
+
201
+ // Check if already in selectedTable (optional but good)
202
+ var alreadyExists = false;
203
+ selectedTable.rows().every(function() {{
204
+ if(this.data()[1] === data[1]) {{ alreadyExists = true; }}
205
+ }});
206
+
207
+ if(!alreadyExists) {{
208
+ selectedTable.row.add(newData).draw();
209
+ }}
210
+ }});
211
+
212
+ // Handle Remove button
213
+ $('#selected_table tbody').on('click', '.remove-btn', function () {{
214
+ selectedTable.row($(this).parents('tr')).remove().draw();
215
+ }});
216
+
217
+ // Handle Clear All
218
+ $('#clear_all').on('click', function() {{
219
+ selectedTable.clear().draw();
220
+ }});
221
+ }});
222
+ </script>
223
+ </body>
224
+ </html>
225
+ """
226
+
227
+ final_html_path = os.path.join(outdir, outfile)
228
+ with open(final_html_path, "w") as f:
229
+ f.write(final_html)
230
+
231
+ pprint_local_path(
232
+ final_html_path, get_wins_path=True, tag="PlotlyUtils.parallel_plot"
233
+ )
234
+ return fig
@@ -0,0 +1,140 @@
1
+ import glob
2
+ import wandb
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Optional, Literal
6
+
7
+ from tap import Tap
8
+ from tqdm import tqdm
9
+
10
+ from rich.console import Console
11
+ import yaml
12
+
13
+ wandb_console = Console()
14
+
15
+
16
+ # --- Argument Parser ---
17
+ class WandbArgs(Tap):
18
+ # --- Auth ---
19
+ api_key_yaml: Path = Path(
20
+ "E:/Dev/__halib/.env.yaml"
21
+ ) # Path to YAML file containing W&B API key
22
+
23
+ # --- Operations ---
24
+ op: Literal["sync", "delete"] = "delete" # Operation to perform
25
+
26
+ # --- Project & Filtering ---
27
+ project: str = "paper2_main" # W&B project name
28
+ pattern: Optional[str] = None # Run name pattern to match for deletion
29
+
30
+ # --- Paths ---
31
+ outdir: Path = Path("./zout/zruns") # Directory containing runs to sync
32
+
33
+ def configure(self):
34
+ self.add_argument("-prj", "--project")
35
+ self.add_argument("-pt", "--pattern")
36
+ self.add_argument("-o", "--outdir")
37
+ self.add_argument("-k", "--api_key_yaml")
38
+
39
+ def validate(self):
40
+ if self.op == "sync" and not self.outdir.exists():
41
+ raise ValueError(f"Output directory {self.outdir} does not exist.")
42
+ if self.op == "delete" and not self.project.strip():
43
+ raise ValueError("Project name must be a non-empty string.")
44
+
45
+
46
+ def login_wandb(api_key: Optional[str]):
47
+ """Handles authentication with Weights & Biases."""
48
+ try:
49
+ if api_key:
50
+ # Explicit login with key
51
+ wandb.login(key=api_key)
52
+ wandb_console.print(
53
+ "[green]Successfully logged into W&B using provided API key.[/green]"
54
+ )
55
+ else:
56
+ # Attempts to find key in environment variables or netrc file
57
+ if wandb.login():
58
+ wandb_console.print(
59
+ "[blue]Logged into W&B using existing credentials.[/blue]"
60
+ )
61
+ else:
62
+ wandb_console.print(
63
+ "[red]Authentication failed. Please provide an API key with -k.[/red]"
64
+ )
65
+ exit(1)
66
+ except Exception as e:
67
+ wandb_console.print(f"[bold red]Login Error:[/bold red] {e}")
68
+ exit(1)
69
+
70
+
71
+ # --- Logic Functions ---
72
+
73
+
74
+ def sync_runs(outdir: Path):
75
+ outdir_path = outdir.absolute()
76
+ sub_dirs = [d for d in outdir_path.iterdir() if d.is_dir()]
77
+
78
+ if not sub_dirs:
79
+ wandb_console.print(f"[red]No subdirectories found in {outdir_path}.[/red]")
80
+ return
81
+
82
+ wandb_console.rule(f"Syncing from {outdir_path}")
83
+
84
+ wandb_dirs = []
85
+ for exp_dir in sub_dirs:
86
+ wandb_dirs.extend(glob.glob(str(exp_dir / "wandb" / "*run-*")))
87
+
88
+ if not wandb_dirs:
89
+ wandb_console.print("No wandb runs found.")
90
+ return
91
+
92
+ for i, wandb_dir in enumerate(wandb_dirs):
93
+ wandb_console.print(f"[{i+1}/{len(wandb_dirs)}] Syncing: {wandb_dir}")
94
+
95
+ # Note: 'wandb sync' command uses the credentials from wandb.login()
96
+ process = subprocess.Popen(
97
+ ["wandb", "sync", wandb_dir],
98
+ stdout=subprocess.PIPE,
99
+ stderr=subprocess.STDOUT,
100
+ text=True,
101
+ )
102
+ for line in process.stdout: # ty:ignore[not-iterable]
103
+ if "ERROR" in line:
104
+ wandb_console.print(f"[red]{line.strip()}[/red]")
105
+ process.wait()
106
+
107
+
108
+ def delete_runs(project: str, pattern: Optional[str]):
109
+ api = wandb.Api()
110
+ runs = api.runs(project)
111
+
112
+ deleted = 0
113
+ for run in tqdm(runs, desc="Processing runs"):
114
+ if pattern is None or pattern in run.name:
115
+ run.delete()
116
+ deleted += 1
117
+ wandb_console.print(f"[green]Total runs deleted: {deleted}[/green]")
118
+
119
+
120
+ def main():
121
+ args = WandbArgs().parse_args()
122
+ cfg_dict = {}
123
+ with open(args.api_key_yaml, "r") as f:
124
+ cfg_dict = yaml.safe_load(f)
125
+ wandb_api_key = cfg_dict.get("WANDB_API_KEY")
126
+ assert (
127
+ wandb_api_key is not None
128
+ ), "W&B API key not found in the specified YAML file."
129
+ # 1. Login first
130
+ login_wandb(wandb_api_key)
131
+
132
+ # 2. Execute operation
133
+ if args.op == "sync":
134
+ sync_runs(args.outdir)
135
+ elif args.op == "delete":
136
+ delete_runs(args.project, args.pattern)
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.32
3
+ Version: 0.2.34
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -57,7 +57,12 @@ Dynamic: summary
57
57
 
58
58
  ## v0.2.x (Experiment & Core Updates)
59
59
 
60
- ### **v0.2.32**
60
+ ### **v0.2.34**
61
+
62
+ - ✨ **New Feature:**: introduce `utils.PlotlyUtils` with parallel coordinates plot and data table support
63
+
64
+ - 🚀 **Improvement:**: move `wandb_op.py` to `utils` and add `scripts` folder
65
+
61
66
 
62
67
  - ✨ **New Feature:**: add `common.common.log_func` as decorator to log function entry, exit, with execution time and arguments.
63
68
 
@@ -18,7 +18,6 @@ halib/exp/core/__init__.py
18
18
  halib/exp/core/base_config.py
19
19
  halib/exp/core/base_exp.py
20
20
  halib/exp/core/param_gen.py
21
- halib/exp/core/wandb_op.py
22
21
  halib/exp/data/__init__.py
23
22
  halib/exp/data/dataclass_util.py
24
23
  halib/exp/data/dataset.py
@@ -52,4 +51,6 @@ halib/system/path.py
52
51
  halib/utils/__init__.py
53
52
  halib/utils/dict.py
54
53
  halib/utils/list.py
55
- halib/utils/slack.py
54
+ halib/utils/plotly_op.py
55
+ halib/utils/slack_op.py
56
+ halib/utils/wandb_op.py
@@ -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.32",
11
+ version="0.2.34",
12
12
  author="Hoang Van Ha",
13
13
  author_email="hoangvanhauit@gmail.com",
14
14
  description="Small library for common tasks",
halib-0.2.32/MANIFEST.in DELETED
@@ -1,5 +0,0 @@
1
- prune _archived
2
- prune test
3
- prune zout
4
- exclude *.toml
5
- include halib/system/_list_pc.csv
@@ -1,117 +0,0 @@
1
- import os
2
- import glob
3
- import wandb
4
- import argparse
5
- import subprocess
6
-
7
- from tqdm import tqdm
8
- from rich.console import Console
9
-
10
- console = Console()
11
-
12
- def sync_runs(outdir):
13
- outdir = os.path.abspath(outdir)
14
- assert os.path.exists(outdir), f"Output directory {outdir} does not exist."
15
- sub_dirs = [name for name in os.listdir(outdir) if os.path.isdir(os.path.join(outdir, name))]
16
- assert len(sub_dirs) > 0, f"No subdirectories found in {outdir}."
17
- console.rule("Parent Directory")
18
- console.print(f"[yellow]{outdir}[/yellow]")
19
-
20
- exp_dirs = [os.path.join(outdir, sub_dir) for sub_dir in sub_dirs]
21
- wandb_dirs = []
22
- for exp_dir in exp_dirs:
23
- wandb_dirs.extend(glob.glob(f"{exp_dir}/wandb/*run-*"))
24
- if len(wandb_dirs) == 0:
25
- console.print(f"No wandb runs found in {outdir}.")
26
- return
27
- else:
28
- console.print(f"Found [bold]{len(wandb_dirs)}[/bold] wandb runs in {outdir}.")
29
- for i, wandb_dir in enumerate(wandb_dirs):
30
- console.rule(f"Syncing wandb run {i + 1}/{len(wandb_dirs)}")
31
- console.print(f"Syncing: {wandb_dir}")
32
- process = subprocess.Popen(
33
- ["wandb", "sync", wandb_dir],
34
- stdout=subprocess.PIPE,
35
- stderr=subprocess.STDOUT,
36
- text=True,
37
- )
38
-
39
- for line in process.stdout:
40
- console.print(line.strip())
41
- if " ERROR Error while calling W&B API" in line:
42
- break
43
- process.stdout.close()
44
- process.wait()
45
- if process.returncode != 0:
46
- console.print(f"[red]Error syncing {wandb_dir}. Return code: {process.returncode}[/red]")
47
- else:
48
- console.print(f"Successfully synced {wandb_dir}.")
49
-
50
- def delete_runs(project, pattern=None):
51
- console.rule("Delete W&B Runs")
52
- confirm_msg = f"Are you sure you want to delete all runs in"
53
- confirm_msg += f" \n\tproject: [red]{project}[/red]"
54
- if pattern:
55
- confirm_msg += f"\n\tpattern: [blue]{pattern}[/blue]"
56
-
57
- console.print(confirm_msg)
58
- confirmation = input(f"This action cannot be undone. [y/N]: ").strip().lower()
59
- if confirmation != "y":
60
- print("Cancelled.")
61
- return
62
-
63
- print("Confirmed. Proceeding...")
64
- api = wandb.Api()
65
- runs = api.runs(project)
66
-
67
- deleted = 0
68
- console.rule("Deleting W&B Runs")
69
- if len(runs) == 0:
70
- print("No runs found in the project.")
71
- return
72
- for run in tqdm(runs):
73
- if pattern is None or pattern in run.name:
74
- run.delete()
75
- console.print(f"Deleted run: [red]{run.name}[/red]")
76
- deleted += 1
77
-
78
- console.print(f"Total runs deleted: {deleted}")
79
-
80
-
81
- def valid_argument(args):
82
- if args.op == "sync":
83
- assert os.path.exists(args.outdir), f"Output directory {args.outdir} does not exist."
84
- elif args.op == "delete":
85
- assert isinstance(args.project, str) and len(args.project.strip()) > 0, "Project name must be a non-empty string."
86
- else:
87
- raise ValueError(f"Unknown operation: {args.op}")
88
-
89
- def parse_args():
90
- parser = argparse.ArgumentParser(description="Operations on W&B runs")
91
- parser.add_argument("-op", "--op", type=str, help="Operation to perform", default="sync", choices=["delete", "sync"])
92
- parser.add_argument("-prj", "--project", type=str, default="fire-paper2-2025", help="W&B project name")
93
- parser.add_argument("-outdir", "--outdir", type=str, help="arg1 description", default="./zout/train")
94
- parser.add_argument("-pt", "--pattern",
95
- type=str,
96
- default=None,
97
- help="Run name pattern to match for deletion",
98
- )
99
-
100
- return parser.parse_args()
101
-
102
-
103
- def main():
104
- args = parse_args()
105
- # Validate arguments, stop if invalid
106
- valid_argument(args)
107
-
108
- op = args.op
109
- if op == "sync":
110
- sync_runs(args.outdir)
111
- elif op == "delete":
112
- delete_runs(args.project, args.pattern)
113
- else:
114
- raise ValueError(f"Unknown operation: {op}")
115
-
116
- if __name__ == "__main__":
117
- main()
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