halib 0.2.33__py3-none-any.whl → 0.2.35__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/utils/plotly_op.py +248 -0
- halib/utils/slack_op.py +86 -0
- {halib-0.2.33.dist-info → halib-0.2.35.dist-info}/METADATA +4 -2
- {halib-0.2.33.dist-info → halib-0.2.35.dist-info}/RECORD +7 -5
- {halib-0.2.33.dist-info → halib-0.2.35.dist-info}/WHEEL +0 -0
- {halib-0.2.33.dist-info → halib-0.2.35.dist-info}/licenses/LICENSE.txt +0 -0
- {halib-0.2.33.dist-info → halib-0.2.35.dist-info}/top_level.txt +0 -0
halib/utils/plotly_op.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
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
|
+
cols = [col for col in cols if col not in exclude_dims]
|
|
113
|
+
|
|
114
|
+
df_display = df[cols].copy()
|
|
115
|
+
df_display.insert(0, "Selection", '<button class="select-btn">Select</button>')
|
|
116
|
+
|
|
117
|
+
chart_html = fig.to_html(full_html=False, include_plotlyjs="cdn")
|
|
118
|
+
|
|
119
|
+
table_html = df_display.to_html(
|
|
120
|
+
classes="display nowrap", table_id="exp_table", index=False, escape=False
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
final_html = f"""
|
|
124
|
+
<!DOCTYPE html>
|
|
125
|
+
<html>
|
|
126
|
+
<head>
|
|
127
|
+
<title>Experiment Dashboard</title>
|
|
128
|
+
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.css">
|
|
129
|
+
<script type="text/javascript" charset="utf8" src="https://code.jquery.com/jquery-3.5.1.js"></script>
|
|
130
|
+
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.js"></script>
|
|
131
|
+
<style>
|
|
132
|
+
body {{
|
|
133
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
134
|
+
margin: 20px;
|
|
135
|
+
background-color: #f0f2f5;
|
|
136
|
+
}}
|
|
137
|
+
.container {{
|
|
138
|
+
background: white;
|
|
139
|
+
padding: 30px;
|
|
140
|
+
border-radius: 12px;
|
|
141
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
|
142
|
+
}}
|
|
143
|
+
|
|
144
|
+
/* --- TABLE FONT SIZE FIX --- */
|
|
145
|
+
table.dataTable {{
|
|
146
|
+
font-size: 11px; /* Smaller overall text */
|
|
147
|
+
}}
|
|
148
|
+
table.dataTable thead th {{
|
|
149
|
+
background-color: #333;
|
|
150
|
+
color: white;
|
|
151
|
+
padding: 8px !important;
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
}}
|
|
154
|
+
table.dataTable tbody td {{
|
|
155
|
+
padding: 4px 8px !important; /* Compact rows */
|
|
156
|
+
}}
|
|
157
|
+
|
|
158
|
+
#exp_table tbody tr:nth-child(even), #selected_table tbody tr:nth-child(even) {{ background-color: #f2f2f2; }}
|
|
159
|
+
#exp_table tbody tr:nth-child(odd), #selected_table tbody tr:nth-child(odd) {{ background-color: #ffffff; }}
|
|
160
|
+
#exp_table tbody tr:hover, #selected_table tbody tr:hover {{ background-color: #e0e0e0; }}
|
|
161
|
+
|
|
162
|
+
h2 {{ color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-top: 40px; }}
|
|
163
|
+
|
|
164
|
+
/* Smaller buttons */
|
|
165
|
+
.select-btn {{ background-color: #28a745; color: white; border: none; padding: 3px 7px; cursor: pointer; border-radius: 4px; font-size: 10px; }}
|
|
166
|
+
.remove-btn {{ background-color: #dc3545; color: white; border: none; padding: 3px 7px; cursor: pointer; border-radius: 4px; font-size: 10px; }}
|
|
167
|
+
.clear-btn {{ background-color: #6c757d; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px; margin-bottom: 10px; font-size: 12px; }}
|
|
168
|
+
</style>
|
|
169
|
+
</head>
|
|
170
|
+
<body>
|
|
171
|
+
<div class="container">
|
|
172
|
+
{chart_html}
|
|
173
|
+
|
|
174
|
+
<h2>Selected Experiments</h2>
|
|
175
|
+
<button id="clear_all" class="clear-btn">Clear All</button>
|
|
176
|
+
<div style="overflow-x:auto; margin-bottom: 40px;">
|
|
177
|
+
<table id="selected_table" class="display nowrap">
|
|
178
|
+
<thead>{df_display.iloc[:0].to_html(index=False, escape=False).split("<thead>")[1].split("</thead>")[0]}</thead>
|
|
179
|
+
<tbody></tbody>
|
|
180
|
+
</table>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<hr>
|
|
184
|
+
|
|
185
|
+
<h2>Full Experiment Raw Data</h2>
|
|
186
|
+
<div style="overflow-x:auto;">
|
|
187
|
+
{table_html}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<script>
|
|
192
|
+
$(document).ready( function () {{
|
|
193
|
+
var mainTable = $('#exp_table').DataTable({{
|
|
194
|
+
"pageLength": 25,
|
|
195
|
+
"order": [[ 5, "desc" ]],
|
|
196
|
+
"stripeClasses": []
|
|
197
|
+
}});
|
|
198
|
+
|
|
199
|
+
var selectedTable = $('#selected_table').DataTable({{
|
|
200
|
+
"paging": false,
|
|
201
|
+
"searching": false,
|
|
202
|
+
"info": false,
|
|
203
|
+
"stripeClasses": []
|
|
204
|
+
}});
|
|
205
|
+
|
|
206
|
+
// Handle Select button
|
|
207
|
+
$('#exp_table tbody').on('click', '.select-btn', function () {{
|
|
208
|
+
var data = mainTable.row($(this).parents('tr')).data();
|
|
209
|
+
var rowNode = $(this).parents('tr');
|
|
210
|
+
|
|
211
|
+
// Change button to remove in the new table
|
|
212
|
+
var newData = [...data];
|
|
213
|
+
newData[0] = '<button class="remove-btn">Remove</button>';
|
|
214
|
+
|
|
215
|
+
// Check if already in selectedTable (optional but good)
|
|
216
|
+
var alreadyExists = false;
|
|
217
|
+
selectedTable.rows().every(function() {{
|
|
218
|
+
if(this.data()[1] === data[1]) {{ alreadyExists = true; }}
|
|
219
|
+
}});
|
|
220
|
+
|
|
221
|
+
if(!alreadyExists) {{
|
|
222
|
+
selectedTable.row.add(newData).draw();
|
|
223
|
+
}}
|
|
224
|
+
}});
|
|
225
|
+
|
|
226
|
+
// Handle Remove button
|
|
227
|
+
$('#selected_table tbody').on('click', '.remove-btn', function () {{
|
|
228
|
+
selectedTable.row($(this).parents('tr')).remove().draw();
|
|
229
|
+
}});
|
|
230
|
+
|
|
231
|
+
// Handle Clear All
|
|
232
|
+
$('#clear_all').on('click', function() {{
|
|
233
|
+
selectedTable.clear().draw();
|
|
234
|
+
}});
|
|
235
|
+
}});
|
|
236
|
+
</script>
|
|
237
|
+
</body>
|
|
238
|
+
</html>
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
final_html_path = os.path.join(outdir, outfile)
|
|
242
|
+
with open(final_html_path, "w") as f:
|
|
243
|
+
f.write(final_html)
|
|
244
|
+
|
|
245
|
+
pprint_local_path(
|
|
246
|
+
final_html_path, get_wins_path=True, tag="PlotlyUtils.parallel_plot"
|
|
247
|
+
)
|
|
248
|
+
return fig
|
halib/utils/slack_op.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from slack_sdk import WebClient
|
|
3
|
+
from slack_sdk.errors import SlackApiError
|
|
4
|
+
from rich.pretty import pprint
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Utilities for interacting with Slack for experiment notification via Wandb Logger.
|
|
8
|
+
"""
|
|
9
|
+
class SlackUtils:
|
|
10
|
+
_instance = None
|
|
11
|
+
|
|
12
|
+
def __new__(cls, token=None):
|
|
13
|
+
"""
|
|
14
|
+
Singleton __new__ method.
|
|
15
|
+
Ensures only one instance of SlackUtils exists.
|
|
16
|
+
"""
|
|
17
|
+
if cls._instance is None:
|
|
18
|
+
if token is None:
|
|
19
|
+
raise ValueError(
|
|
20
|
+
"A Slack Token is required for the first initialization."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Create the instance
|
|
24
|
+
cls._instance = super(SlackUtils, cls).__new__(cls)
|
|
25
|
+
|
|
26
|
+
# Initialize the WebClient only once
|
|
27
|
+
cls._instance.client = WebClient(token=token)
|
|
28
|
+
cls._instance.token = token
|
|
29
|
+
|
|
30
|
+
return cls._instance
|
|
31
|
+
|
|
32
|
+
def clear_channel(self, channel_id, sleep_interval=1.0):
|
|
33
|
+
"""
|
|
34
|
+
Fetches and deletes all messages in a specified channel.
|
|
35
|
+
"""
|
|
36
|
+
cursor = None
|
|
37
|
+
deleted_count = 0
|
|
38
|
+
|
|
39
|
+
pprint(f"--- Starting cleanup for Channel ID: {channel_id} ---")
|
|
40
|
+
|
|
41
|
+
while True:
|
|
42
|
+
try:
|
|
43
|
+
# Fetch history in batches of 100
|
|
44
|
+
response = self.client.conversations_history( # ty:ignore[unresolved-attribute]
|
|
45
|
+
channel=channel_id, cursor=cursor, limit=100
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
messages = response.get("messages", [])
|
|
49
|
+
|
|
50
|
+
if not messages:
|
|
51
|
+
pprint("No more messages found to delete.")
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
for msg in messages:
|
|
55
|
+
ts = msg.get("ts")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# Attempt delete
|
|
59
|
+
self.client.chat_delete( # ty:ignore[unresolved-attribute]
|
|
60
|
+
channel=channel_id, ts=ts
|
|
61
|
+
)
|
|
62
|
+
pprint(f"Deleted: {ts}")
|
|
63
|
+
deleted_count += 1
|
|
64
|
+
|
|
65
|
+
# Rate limit protection (Tier 3 limit)
|
|
66
|
+
time.sleep(sleep_interval)
|
|
67
|
+
|
|
68
|
+
except SlackApiError as e:
|
|
69
|
+
error_code = e.response["error"]
|
|
70
|
+
if error_code == "cant_delete_message":
|
|
71
|
+
pprint(f"Skipped (Permission denied): {ts}")
|
|
72
|
+
elif error_code == "message_not_found":
|
|
73
|
+
pprint(f"Skipped (Already deleted): {ts}")
|
|
74
|
+
else:
|
|
75
|
+
pprint(f"Error deleting {ts}: {error_code}")
|
|
76
|
+
# Check for pagination
|
|
77
|
+
if response["has_more"]:
|
|
78
|
+
cursor = response["response_metadata"]["next_cursor"]
|
|
79
|
+
else:
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
except SlackApiError as e:
|
|
83
|
+
print(f"Critical API Error fetching history: {e.response['error']}")
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
print(f"--- Completed. Total messages deleted: {deleted_count} ---")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: halib
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.35
|
|
4
4
|
Summary: Small library for common tasks
|
|
5
5
|
Author: Hoang Van Ha
|
|
6
6
|
Author-email: hoangvanhauit@gmail.com
|
|
@@ -57,7 +57,9 @@ Dynamic: summary
|
|
|
57
57
|
|
|
58
58
|
## v0.2.x (Experiment & Core Updates)
|
|
59
59
|
|
|
60
|
-
### **v0.2.
|
|
60
|
+
### **v0.2.35**
|
|
61
|
+
|
|
62
|
+
- ✨ **New Feature:**: introduce `utils.PlotlyUtils` with parallel coordinates plot and data table support
|
|
61
63
|
|
|
62
64
|
- 🚀 **Improvement:**: move `wandb_op.py` to `utils` and add `scripts` folder
|
|
63
65
|
|
|
@@ -100,12 +100,14 @@ 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/plotly_op.py,sha256=wcyMPFavTOv8gbcx82H-_T5EzpUDCqGRH-sQO9NAk4M,9883
|
|
103
104
|
halib/utils/slack.py,sha256=2ugWE_eJ0s479ObACJbx7iEu3kjMPD4Rt2hEwuMpuNQ,3099
|
|
105
|
+
halib/utils/slack_op.py,sha256=2ugWE_eJ0s479ObACJbx7iEu3kjMPD4Rt2hEwuMpuNQ,3099
|
|
104
106
|
halib/utils/tele_noti.py,sha256=-4WXZelCA4W9BroapkRyIdUu9cUVrcJJhegnMs_WpGU,5928
|
|
105
107
|
halib/utils/video.py,sha256=zLoj5EHk4SmP9OnoHjO8mLbzPdtq6gQPzTQisOEDdO8,3261
|
|
106
108
|
halib/utils/wandb_op.py,sha256=qqDdTMW4J07bzuJTTg2HoLAPs21nVEbwt2-Aa5ZKiVk,4336
|
|
107
|
-
halib-0.2.
|
|
108
|
-
halib-0.2.
|
|
109
|
-
halib-0.2.
|
|
110
|
-
halib-0.2.
|
|
111
|
-
halib-0.2.
|
|
109
|
+
halib-0.2.35.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
|
|
110
|
+
halib-0.2.35.dist-info/METADATA,sha256=nG6m8r1FkN3qxFpBz_FBMM8JTUmCgAeYvBxVOYHFer4,8379
|
|
111
|
+
halib-0.2.35.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
112
|
+
halib-0.2.35.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
|
|
113
|
+
halib-0.2.35.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|