yanex 0.1.0__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.
- yanex/__init__.py +74 -0
- yanex/api.py +507 -0
- yanex/cli/__init__.py +3 -0
- yanex/cli/_utils.py +114 -0
- yanex/cli/commands/__init__.py +3 -0
- yanex/cli/commands/archive.py +177 -0
- yanex/cli/commands/compare.py +320 -0
- yanex/cli/commands/confirm.py +198 -0
- yanex/cli/commands/delete.py +203 -0
- yanex/cli/commands/list.py +243 -0
- yanex/cli/commands/run.py +625 -0
- yanex/cli/commands/show.py +560 -0
- yanex/cli/commands/unarchive.py +177 -0
- yanex/cli/commands/update.py +282 -0
- yanex/cli/filters/__init__.py +8 -0
- yanex/cli/filters/base.py +286 -0
- yanex/cli/filters/time_utils.py +178 -0
- yanex/cli/formatters/__init__.py +7 -0
- yanex/cli/formatters/console.py +325 -0
- yanex/cli/main.py +45 -0
- yanex/core/__init__.py +3 -0
- yanex/core/comparison.py +549 -0
- yanex/core/config.py +587 -0
- yanex/core/constants.py +16 -0
- yanex/core/environment.py +146 -0
- yanex/core/git_utils.py +153 -0
- yanex/core/manager.py +555 -0
- yanex/core/storage.py +682 -0
- yanex/ui/__init__.py +1 -0
- yanex/ui/compare_table.py +524 -0
- yanex/utils/__init__.py +3 -0
- yanex/utils/exceptions.py +70 -0
- yanex/utils/validation.py +165 -0
- yanex-0.1.0.dist-info/METADATA +251 -0
- yanex-0.1.0.dist-info/RECORD +39 -0
- yanex-0.1.0.dist-info/WHEEL +5 -0
- yanex-0.1.0.dist-info/entry_points.txt +2 -0
- yanex-0.1.0.dist-info/licenses/LICENSE +21 -0
- yanex-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,524 @@
|
|
1
|
+
"""
|
2
|
+
Interactive comparison table using Textual DataTable.
|
3
|
+
|
4
|
+
This module provides a terminal-based interactive table for comparing experiments,
|
5
|
+
with sorting, navigation, and export functionality.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import csv
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Dict, List, Optional
|
11
|
+
|
12
|
+
from textual.app import App, ComposeResult
|
13
|
+
from textual.binding import Binding
|
14
|
+
from textual.containers import Container, Horizontal
|
15
|
+
from textual.screen import ModalScreen
|
16
|
+
from textual.widgets import Button, DataTable, Footer, Header, Input, Label, Static
|
17
|
+
|
18
|
+
|
19
|
+
class HelpScreen(ModalScreen):
|
20
|
+
"""Modal screen showing keyboard shortcuts and help."""
|
21
|
+
|
22
|
+
def compose(self) -> ComposeResult:
|
23
|
+
"""Compose the help screen layout."""
|
24
|
+
yield Container(
|
25
|
+
Label("yanex compare - Keyboard Shortcuts", classes="help-title"),
|
26
|
+
Static(
|
27
|
+
"""
|
28
|
+
Navigation:
|
29
|
+
↑/↓, j/k Navigate rows
|
30
|
+
←/→, h/l Navigate columns
|
31
|
+
Home/End Jump to first/last row
|
32
|
+
PgUp/PgDn Navigate by page
|
33
|
+
|
34
|
+
Sorting:
|
35
|
+
s Sort ascending by current column
|
36
|
+
S Sort descending by current column
|
37
|
+
1 Numerical sort ascending
|
38
|
+
2 Numerical sort descending
|
39
|
+
r Reset to original order
|
40
|
+
R Reverse current sort order
|
41
|
+
|
42
|
+
Other Controls:
|
43
|
+
e Export current view to CSV
|
44
|
+
? Show this help
|
45
|
+
q, Ctrl+C Quit
|
46
|
+
|
47
|
+
Column Types:
|
48
|
+
Parameters are prefixed with 'param:'
|
49
|
+
Metrics are prefixed with 'metric:'
|
50
|
+
Missing values are shown as '-'
|
51
|
+
""",
|
52
|
+
classes="help-content",
|
53
|
+
),
|
54
|
+
Button("Close", variant="primary", id="close-help"),
|
55
|
+
classes="help-dialog",
|
56
|
+
)
|
57
|
+
|
58
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
59
|
+
"""Handle button press events."""
|
60
|
+
if event.button.id == "close-help":
|
61
|
+
self.dismiss()
|
62
|
+
|
63
|
+
|
64
|
+
class ExportScreen(ModalScreen):
|
65
|
+
"""Modal screen for CSV export."""
|
66
|
+
|
67
|
+
def __init__(self, default_path: str = "comparison.csv"):
|
68
|
+
super().__init__()
|
69
|
+
self.default_path = default_path
|
70
|
+
|
71
|
+
def compose(self) -> ComposeResult:
|
72
|
+
"""Compose the export screen layout."""
|
73
|
+
yield Container(
|
74
|
+
Label("Export Comparison Data", classes="export-title"),
|
75
|
+
Label("Enter filename for CSV export:"),
|
76
|
+
Input(
|
77
|
+
value=self.default_path, placeholder="comparison.csv", id="export-path"
|
78
|
+
),
|
79
|
+
Horizontal(
|
80
|
+
Button("Export", variant="primary", id="export-confirm"),
|
81
|
+
Button("Cancel", variant="default", id="export-cancel"),
|
82
|
+
classes="export-buttons",
|
83
|
+
),
|
84
|
+
classes="export-dialog",
|
85
|
+
)
|
86
|
+
|
87
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
88
|
+
"""Handle button press events."""
|
89
|
+
if event.button.id == "export-confirm":
|
90
|
+
path_input = self.query_one("#export-path", Input)
|
91
|
+
self.dismiss(path_input.value)
|
92
|
+
elif event.button.id == "export-cancel":
|
93
|
+
self.dismiss(None)
|
94
|
+
|
95
|
+
|
96
|
+
class ComparisonTableApp(App):
|
97
|
+
"""Interactive comparison table application."""
|
98
|
+
|
99
|
+
CSS = """
|
100
|
+
.help-dialog {
|
101
|
+
width: 80;
|
102
|
+
height: 30;
|
103
|
+
background: $surface;
|
104
|
+
border: thick $primary;
|
105
|
+
margin: 2;
|
106
|
+
padding: 1;
|
107
|
+
}
|
108
|
+
|
109
|
+
.help-title {
|
110
|
+
text-align: center;
|
111
|
+
text-style: bold;
|
112
|
+
color: $primary;
|
113
|
+
margin-bottom: 1;
|
114
|
+
}
|
115
|
+
|
116
|
+
.help-content {
|
117
|
+
margin: 1;
|
118
|
+
padding: 1;
|
119
|
+
background: $surface-darken-1;
|
120
|
+
}
|
121
|
+
|
122
|
+
.export-dialog {
|
123
|
+
width: 60;
|
124
|
+
height: 12;
|
125
|
+
background: $surface;
|
126
|
+
border: thick $primary;
|
127
|
+
margin: 2;
|
128
|
+
padding: 1;
|
129
|
+
}
|
130
|
+
|
131
|
+
.export-title {
|
132
|
+
text-align: center;
|
133
|
+
text-style: bold;
|
134
|
+
color: $primary;
|
135
|
+
margin-bottom: 1;
|
136
|
+
}
|
137
|
+
|
138
|
+
.export-buttons {
|
139
|
+
margin-top: 1;
|
140
|
+
align: center middle;
|
141
|
+
}
|
142
|
+
|
143
|
+
DataTable {
|
144
|
+
height: 1fr;
|
145
|
+
}
|
146
|
+
|
147
|
+
#status-bar {
|
148
|
+
height: 1;
|
149
|
+
background: $surface-darken-1;
|
150
|
+
color: $text-muted;
|
151
|
+
}
|
152
|
+
"""
|
153
|
+
|
154
|
+
BINDINGS = [
|
155
|
+
Binding("q", "quit", "Quit"),
|
156
|
+
Binding("question_mark", "help", "Help"),
|
157
|
+
Binding("s", "sort_asc", "Sort ↑"),
|
158
|
+
Binding("S", "sort_desc", "Sort ↓"),
|
159
|
+
Binding("1", "sort_numeric_asc", "Numeric ↑"),
|
160
|
+
Binding("2", "sort_numeric_desc", "Numeric ↓"),
|
161
|
+
Binding("r", "reset_sort", "Reset"),
|
162
|
+
Binding("R", "reverse_sort", "Reverse"),
|
163
|
+
Binding("e", "export", "Export"),
|
164
|
+
]
|
165
|
+
|
166
|
+
def __init__(
|
167
|
+
self,
|
168
|
+
comparison_data: Dict[str, Any],
|
169
|
+
title: str = "yanex compare",
|
170
|
+
export_path: Optional[str] = None,
|
171
|
+
):
|
172
|
+
"""
|
173
|
+
Initialize the comparison table app.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
comparison_data: Data from ExperimentComparisonData.get_comparison_data()
|
177
|
+
title: Application title
|
178
|
+
export_path: Default export path for CSV
|
179
|
+
"""
|
180
|
+
super().__init__()
|
181
|
+
self.comparison_data = comparison_data
|
182
|
+
self.title_text = title
|
183
|
+
self.export_path = export_path or "comparison.csv"
|
184
|
+
self.original_rows = comparison_data.get("rows", [])
|
185
|
+
# Set default sort to "started" descending (latest experiments first)
|
186
|
+
self.current_sort_key = "started"
|
187
|
+
self.current_sort_reverse = True
|
188
|
+
|
189
|
+
def compose(self) -> ComposeResult:
|
190
|
+
"""Compose the application layout."""
|
191
|
+
yield Header(show_clock=True)
|
192
|
+
yield DataTable(id="comparison-table")
|
193
|
+
yield Static("", id="status-bar")
|
194
|
+
yield Footer()
|
195
|
+
|
196
|
+
def on_mount(self) -> None:
|
197
|
+
"""Initialize the table when app mounts."""
|
198
|
+
self.title = self.title_text
|
199
|
+
|
200
|
+
# Apply default sort (started descending) - this will also populate the table
|
201
|
+
self._sort_table("started", reverse=True)
|
202
|
+
|
203
|
+
def _setup_table_columns(self, table: DataTable) -> None:
|
204
|
+
"""Set up table columns."""
|
205
|
+
rows = self.comparison_data.get("rows", [])
|
206
|
+
if not rows:
|
207
|
+
return
|
208
|
+
|
209
|
+
# Get column names from first row
|
210
|
+
first_row = rows[0]
|
211
|
+
column_keys = list(first_row.keys())
|
212
|
+
|
213
|
+
# Add columns to table
|
214
|
+
for key in column_keys:
|
215
|
+
header = self._get_column_header(key)
|
216
|
+
table.add_column(header, key=key)
|
217
|
+
|
218
|
+
def _populate_table_data(self, table: DataTable) -> None:
|
219
|
+
"""Populate table with data."""
|
220
|
+
# Save cursor position before clearing
|
221
|
+
saved_cursor_row = table.cursor_row
|
222
|
+
saved_cursor_column = table.cursor_column
|
223
|
+
|
224
|
+
# Clear table completely (removes columns and rows)
|
225
|
+
table.clear(columns=True)
|
226
|
+
rows = self.comparison_data.get("rows", [])
|
227
|
+
|
228
|
+
if not rows:
|
229
|
+
return
|
230
|
+
|
231
|
+
# Get column order from data
|
232
|
+
column_keys = self._get_column_keys()
|
233
|
+
|
234
|
+
# Re-add columns with updated headers (including sort indicators)
|
235
|
+
for key in column_keys:
|
236
|
+
header = self._get_column_header(key)
|
237
|
+
table.add_column(header, key=key)
|
238
|
+
|
239
|
+
# Add rows
|
240
|
+
for row_data in rows:
|
241
|
+
row_values = [row_data.get(key, "-") for key in column_keys]
|
242
|
+
table.add_row(*row_values)
|
243
|
+
|
244
|
+
# Restore cursor position (with bounds checking)
|
245
|
+
if rows:
|
246
|
+
max_row = len(rows) - 1
|
247
|
+
max_column = len(column_keys) - 1
|
248
|
+
|
249
|
+
# Ensure cursor position is within bounds
|
250
|
+
new_cursor_row = min(saved_cursor_row, max_row)
|
251
|
+
new_cursor_column = min(saved_cursor_column, max_column)
|
252
|
+
|
253
|
+
# Restore cursor position
|
254
|
+
table.move_cursor(row=new_cursor_row, column=new_cursor_column)
|
255
|
+
|
256
|
+
def _get_column_keys(self) -> List[str]:
|
257
|
+
"""Get column keys from data."""
|
258
|
+
rows = self.comparison_data.get("rows", [])
|
259
|
+
if not rows:
|
260
|
+
return []
|
261
|
+
return list(rows[0].keys())
|
262
|
+
|
263
|
+
def _get_column_header(self, key: str) -> str:
|
264
|
+
"""Get formatted column header with optional sort indicator."""
|
265
|
+
# Base header without sort indicator
|
266
|
+
if key.startswith("param:"):
|
267
|
+
base_header = f"📊 {key[6:]}" # Remove 'param:' prefix
|
268
|
+
elif key.startswith("metric:"):
|
269
|
+
base_header = f"📈 {key[7:]}" # Remove 'metric:' prefix
|
270
|
+
elif key == "duration":
|
271
|
+
base_header = "Duration"
|
272
|
+
elif key == "tags":
|
273
|
+
base_header = "Tags"
|
274
|
+
elif key == "id":
|
275
|
+
base_header = "ID"
|
276
|
+
elif key == "name":
|
277
|
+
base_header = "Name"
|
278
|
+
else:
|
279
|
+
base_header = key.title()
|
280
|
+
|
281
|
+
# Add sort indicator if this column is currently sorted
|
282
|
+
if self.current_sort_key == key:
|
283
|
+
sort_indicator = " ↓" if self.current_sort_reverse else " ↑"
|
284
|
+
return base_header + sort_indicator
|
285
|
+
|
286
|
+
return base_header
|
287
|
+
|
288
|
+
def _update_status_bar(self) -> None:
|
289
|
+
"""Update the status bar with current information."""
|
290
|
+
total_experiments = self.comparison_data.get("total_experiments", 0)
|
291
|
+
param_count = len(self.comparison_data.get("param_columns", []))
|
292
|
+
metric_count = len(self.comparison_data.get("metric_columns", []))
|
293
|
+
|
294
|
+
status_text = (
|
295
|
+
f"Experiments: {total_experiments} | "
|
296
|
+
f"Parameters: {param_count} | "
|
297
|
+
f"Metrics: {metric_count}"
|
298
|
+
)
|
299
|
+
|
300
|
+
if self.current_sort_key:
|
301
|
+
sort_direction = "↓" if self.current_sort_reverse else "↑"
|
302
|
+
status_text += f" | Sorted by: {self.current_sort_key} {sort_direction}"
|
303
|
+
|
304
|
+
status_bar = self.query_one("#status-bar", Static)
|
305
|
+
status_bar.update(status_text)
|
306
|
+
|
307
|
+
def action_help(self) -> None:
|
308
|
+
"""Show help screen."""
|
309
|
+
self.push_screen(HelpScreen())
|
310
|
+
|
311
|
+
def action_sort_asc(self) -> None:
|
312
|
+
"""Sort by current column ascending."""
|
313
|
+
table = self.query_one(DataTable)
|
314
|
+
column_keys = self._get_column_keys()
|
315
|
+
if table.cursor_column < len(column_keys):
|
316
|
+
column_key = column_keys[table.cursor_column]
|
317
|
+
self._sort_table(column_key, reverse=False)
|
318
|
+
|
319
|
+
def action_sort_desc(self) -> None:
|
320
|
+
"""Sort by current column descending."""
|
321
|
+
table = self.query_one(DataTable)
|
322
|
+
column_keys = self._get_column_keys()
|
323
|
+
if table.cursor_column < len(column_keys):
|
324
|
+
column_key = column_keys[table.cursor_column]
|
325
|
+
self._sort_table(column_key, reverse=True)
|
326
|
+
|
327
|
+
def action_sort_numeric_asc(self) -> None:
|
328
|
+
"""Sort by current column numerically ascending."""
|
329
|
+
table = self.query_one(DataTable)
|
330
|
+
column_keys = self._get_column_keys()
|
331
|
+
if table.cursor_column < len(column_keys):
|
332
|
+
column_key = column_keys[table.cursor_column]
|
333
|
+
self._sort_table(column_key, reverse=False, numeric=True)
|
334
|
+
|
335
|
+
def action_sort_numeric_desc(self) -> None:
|
336
|
+
"""Sort by current column numerically descending."""
|
337
|
+
table = self.query_one(DataTable)
|
338
|
+
column_keys = self._get_column_keys()
|
339
|
+
if table.cursor_column < len(column_keys):
|
340
|
+
column_key = column_keys[table.cursor_column]
|
341
|
+
self._sort_table(column_key, reverse=True, numeric=True)
|
342
|
+
|
343
|
+
def action_reset_sort(self) -> None:
|
344
|
+
"""Reset to default sort order (started descending)."""
|
345
|
+
# Reset to default sort (started descending)
|
346
|
+
self._sort_table("started", reverse=True)
|
347
|
+
|
348
|
+
def action_reverse_sort(self) -> None:
|
349
|
+
"""Reverse current sort order."""
|
350
|
+
if self.current_sort_key:
|
351
|
+
# Reverse the current sort
|
352
|
+
self._sort_table(
|
353
|
+
self.current_sort_key, reverse=not self.current_sort_reverse
|
354
|
+
)
|
355
|
+
else:
|
356
|
+
# If no current sort, just reverse the rows and update state
|
357
|
+
self.comparison_data["rows"].reverse()
|
358
|
+
self.current_sort_reverse = not self.current_sort_reverse
|
359
|
+
table = self.query_one(DataTable)
|
360
|
+
self._populate_table_data(table)
|
361
|
+
self._update_status_bar()
|
362
|
+
|
363
|
+
def action_export(self) -> None:
|
364
|
+
"""Export data to CSV."""
|
365
|
+
self.push_screen(ExportScreen(self.export_path), self._handle_export)
|
366
|
+
|
367
|
+
def _handle_export(self, export_path: Optional[str]) -> None:
|
368
|
+
"""Handle export screen result."""
|
369
|
+
if export_path:
|
370
|
+
try:
|
371
|
+
self._export_to_csv(export_path)
|
372
|
+
self.notify(f"Exported to {export_path}", severity="information")
|
373
|
+
except Exception as e:
|
374
|
+
self.notify(f"Export failed: {e}", severity="error")
|
375
|
+
|
376
|
+
def _export_to_csv(self, file_path: str) -> None:
|
377
|
+
"""Export current table data to CSV."""
|
378
|
+
rows = self.comparison_data.get("rows", [])
|
379
|
+
if not rows:
|
380
|
+
raise ValueError("No data to export")
|
381
|
+
|
382
|
+
# Get column order from data
|
383
|
+
column_keys = self._get_column_keys()
|
384
|
+
# For headers, we'll use the display headers we set up
|
385
|
+
column_headers = []
|
386
|
+
for key in column_keys:
|
387
|
+
if key.startswith("param:"):
|
388
|
+
column_headers.append(f"📊 {key[6:]}")
|
389
|
+
elif key.startswith("metric:"):
|
390
|
+
column_headers.append(f"📈 {key[7:]}")
|
391
|
+
elif key == "duration":
|
392
|
+
column_headers.append("Duration")
|
393
|
+
elif key == "tags":
|
394
|
+
column_headers.append("Tags")
|
395
|
+
elif key == "id":
|
396
|
+
column_headers.append("ID")
|
397
|
+
elif key == "name":
|
398
|
+
column_headers.append("Name")
|
399
|
+
else:
|
400
|
+
column_headers.append(key.title())
|
401
|
+
|
402
|
+
# Write CSV
|
403
|
+
path = Path(file_path)
|
404
|
+
with path.open("w", newline="", encoding="utf-8") as csvfile:
|
405
|
+
writer = csv.writer(csvfile)
|
406
|
+
|
407
|
+
# Write header
|
408
|
+
writer.writerow(column_headers)
|
409
|
+
|
410
|
+
# Write data rows
|
411
|
+
for row_data in rows:
|
412
|
+
row_values = [row_data.get(key, "-") for key in column_keys]
|
413
|
+
writer.writerow(row_values)
|
414
|
+
|
415
|
+
def _sort_table(
|
416
|
+
self, column_key: str, reverse: bool = False, numeric: bool = False
|
417
|
+
) -> None:
|
418
|
+
"""Sort table by specified column."""
|
419
|
+
rows = self.comparison_data.get("rows", [])
|
420
|
+
if not rows:
|
421
|
+
return
|
422
|
+
|
423
|
+
def sort_key(row_data: Dict[str, Any]) -> Any:
|
424
|
+
"""Get sort key for a row."""
|
425
|
+
value = row_data.get(column_key, "-")
|
426
|
+
|
427
|
+
# Handle missing values
|
428
|
+
if value == "-":
|
429
|
+
return (
|
430
|
+
""
|
431
|
+
if not numeric
|
432
|
+
else float("-inf")
|
433
|
+
if not reverse
|
434
|
+
else float("inf")
|
435
|
+
)
|
436
|
+
|
437
|
+
# Try numeric conversion if requested
|
438
|
+
if numeric:
|
439
|
+
try:
|
440
|
+
return float(value)
|
441
|
+
except (ValueError, TypeError):
|
442
|
+
return float("-inf") if not reverse else float("inf")
|
443
|
+
|
444
|
+
# Use string comparison
|
445
|
+
return str(value).lower()
|
446
|
+
|
447
|
+
# Sort the rows
|
448
|
+
sorted_rows = sorted(rows, key=sort_key, reverse=reverse)
|
449
|
+
self.comparison_data["rows"] = sorted_rows
|
450
|
+
|
451
|
+
# Update status tracking BEFORE populating table (so headers show indicators)
|
452
|
+
self.current_sort_key = column_key
|
453
|
+
self.current_sort_reverse = reverse
|
454
|
+
|
455
|
+
# Update table display
|
456
|
+
table = self.query_one(DataTable)
|
457
|
+
self._populate_table_data(table)
|
458
|
+
self._update_status_bar()
|
459
|
+
|
460
|
+
|
461
|
+
def run_comparison_table(
|
462
|
+
comparison_data: Dict[str, Any],
|
463
|
+
title: str = "yanex compare",
|
464
|
+
export_path: Optional[str] = None,
|
465
|
+
) -> None:
|
466
|
+
"""
|
467
|
+
Run the interactive comparison table.
|
468
|
+
|
469
|
+
Args:
|
470
|
+
comparison_data: Data from ExperimentComparisonData.get_comparison_data()
|
471
|
+
title: Application title
|
472
|
+
export_path: Default export path for CSV
|
473
|
+
"""
|
474
|
+
app = ComparisonTableApp(comparison_data, title, export_path)
|
475
|
+
app.run()
|
476
|
+
|
477
|
+
|
478
|
+
if __name__ == "__main__":
|
479
|
+
# Example usage with mock data
|
480
|
+
mock_data = {
|
481
|
+
"rows": [
|
482
|
+
{
|
483
|
+
"run": "exp001",
|
484
|
+
"operation": "train.py",
|
485
|
+
"started": "2025-01-01 10:00:00",
|
486
|
+
"time": "01:30:45",
|
487
|
+
"status": "completed",
|
488
|
+
"label": "experiment-1",
|
489
|
+
"param:learning_rate": "0.01",
|
490
|
+
"param:epochs": "10",
|
491
|
+
"metric:accuracy": "0.95",
|
492
|
+
"metric:loss": "0.05",
|
493
|
+
},
|
494
|
+
{
|
495
|
+
"run": "exp002",
|
496
|
+
"operation": "train.py",
|
497
|
+
"started": "2025-01-01 12:00:00",
|
498
|
+
"time": "00:45:30",
|
499
|
+
"status": "failed",
|
500
|
+
"label": "experiment-2",
|
501
|
+
"param:learning_rate": "0.02",
|
502
|
+
"param:epochs": "5",
|
503
|
+
"metric:accuracy": "0.87",
|
504
|
+
"metric:loss": "0.13",
|
505
|
+
},
|
506
|
+
],
|
507
|
+
"param_columns": ["learning_rate", "epochs"],
|
508
|
+
"metric_columns": ["accuracy", "loss"],
|
509
|
+
"column_types": {
|
510
|
+
"run": "string",
|
511
|
+
"operation": "string",
|
512
|
+
"started": "datetime",
|
513
|
+
"time": "string",
|
514
|
+
"status": "string",
|
515
|
+
"label": "string",
|
516
|
+
"param:learning_rate": "numeric",
|
517
|
+
"param:epochs": "numeric",
|
518
|
+
"metric:accuracy": "numeric",
|
519
|
+
"metric:loss": "numeric",
|
520
|
+
},
|
521
|
+
"total_experiments": 2,
|
522
|
+
}
|
523
|
+
|
524
|
+
run_comparison_table(mock_data)
|
yanex/utils/__init__.py
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
"""
|
2
|
+
Custom exceptions for yanex.
|
3
|
+
"""
|
4
|
+
|
5
|
+
|
6
|
+
class YanexError(Exception):
|
7
|
+
"""Base exception for all yanex errors."""
|
8
|
+
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
class ExperimentError(YanexError):
|
13
|
+
"""Errors related to experiment management."""
|
14
|
+
|
15
|
+
pass
|
16
|
+
|
17
|
+
|
18
|
+
class GitError(YanexError):
|
19
|
+
"""Errors related to git operations."""
|
20
|
+
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class ConfigError(YanexError):
|
25
|
+
"""Errors related to configuration loading and validation."""
|
26
|
+
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
30
|
+
class StorageError(YanexError):
|
31
|
+
"""Errors related to file storage operations."""
|
32
|
+
|
33
|
+
pass
|
34
|
+
|
35
|
+
|
36
|
+
class ValidationError(YanexError):
|
37
|
+
"""Errors related to input validation."""
|
38
|
+
|
39
|
+
pass
|
40
|
+
|
41
|
+
|
42
|
+
class ExperimentNotFoundError(ExperimentError):
|
43
|
+
"""Raised when an experiment cannot be found by ID or name."""
|
44
|
+
|
45
|
+
def __init__(self, identifier: str) -> None:
|
46
|
+
super().__init__(f"Experiment not found: {identifier}")
|
47
|
+
self.identifier = identifier
|
48
|
+
|
49
|
+
|
50
|
+
class ExperimentAlreadyRunningError(ExperimentError):
|
51
|
+
"""Raised when trying to start an experiment while another is running."""
|
52
|
+
|
53
|
+
def __init__(self, running_id: str) -> None:
|
54
|
+
super().__init__(f"Another experiment is already running: {running_id}")
|
55
|
+
self.running_id = running_id
|
56
|
+
|
57
|
+
|
58
|
+
class DirtyWorkingDirectoryError(GitError):
|
59
|
+
"""Raised when git working directory is not clean."""
|
60
|
+
|
61
|
+
def __init__(self, changes: list[str]) -> None:
|
62
|
+
change_list = "\n".join(f" - {change}" for change in changes)
|
63
|
+
super().__init__(f"Working directory is not clean:\n{change_list}")
|
64
|
+
self.changes = changes
|
65
|
+
|
66
|
+
|
67
|
+
class ExperimentContextError(ExperimentError):
|
68
|
+
"""Raised when experiment context is used incorrectly."""
|
69
|
+
|
70
|
+
pass
|