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,177 @@
|
|
1
|
+
"""
|
2
|
+
Archive experiments - move them to archived directory.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
import click
|
8
|
+
|
9
|
+
from ...core.constants import EXPERIMENT_STATUSES
|
10
|
+
from ..filters import ExperimentFilter, parse_time_spec
|
11
|
+
from .confirm import (
|
12
|
+
confirm_experiment_operation,
|
13
|
+
find_experiments_by_filters,
|
14
|
+
find_experiments_by_identifiers,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
@click.command("archive")
|
19
|
+
@click.argument("experiment_identifiers", nargs=-1)
|
20
|
+
@click.option(
|
21
|
+
"--status",
|
22
|
+
type=click.Choice(EXPERIMENT_STATUSES),
|
23
|
+
help="Archive experiments with specific status",
|
24
|
+
)
|
25
|
+
@click.option(
|
26
|
+
"--name",
|
27
|
+
"name_pattern",
|
28
|
+
help="Archive experiments matching name pattern (glob syntax)",
|
29
|
+
)
|
30
|
+
@click.option(
|
31
|
+
"--tag", "tags", multiple=True, help="Archive experiments with ALL specified tags"
|
32
|
+
)
|
33
|
+
@click.option(
|
34
|
+
"--started-after",
|
35
|
+
help="Archive experiments started after date/time (e.g., '2025-01-01', 'yesterday', '1 week ago')",
|
36
|
+
)
|
37
|
+
@click.option("--started-before", help="Archive experiments started before date/time")
|
38
|
+
@click.option("--ended-after", help="Archive experiments ended after date/time")
|
39
|
+
@click.option("--ended-before", help="Archive experiments ended before date/time")
|
40
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
41
|
+
@click.pass_context
|
42
|
+
def archive_experiments(
|
43
|
+
ctx,
|
44
|
+
experiment_identifiers: tuple,
|
45
|
+
status: Optional[str],
|
46
|
+
name_pattern: Optional[str],
|
47
|
+
tags: tuple,
|
48
|
+
started_after: Optional[str],
|
49
|
+
started_before: Optional[str],
|
50
|
+
ended_after: Optional[str],
|
51
|
+
ended_before: Optional[str],
|
52
|
+
force: bool,
|
53
|
+
):
|
54
|
+
"""
|
55
|
+
Archive experiments by moving them to archived directory.
|
56
|
+
|
57
|
+
EXPERIMENT_IDENTIFIERS can be experiment IDs or names.
|
58
|
+
If no identifiers provided, experiments are filtered by options.
|
59
|
+
|
60
|
+
Examples:
|
61
|
+
\\b
|
62
|
+
yanex archive exp1 exp2 # Archive specific experiments
|
63
|
+
yanex archive --status failed # Archive all failed experiments
|
64
|
+
yanex archive --status completed --ended-before "1 month ago"
|
65
|
+
yanex archive --name "*training*" # Archive experiments with "training" in name
|
66
|
+
yanex archive --tag experiment-v1 # Archive experiments with specific tag
|
67
|
+
"""
|
68
|
+
try:
|
69
|
+
filter_obj = ExperimentFilter()
|
70
|
+
|
71
|
+
# Validate mutually exclusive targeting
|
72
|
+
has_identifiers = len(experiment_identifiers) > 0
|
73
|
+
has_filters = any(
|
74
|
+
[
|
75
|
+
status,
|
76
|
+
name_pattern,
|
77
|
+
tags,
|
78
|
+
started_after,
|
79
|
+
started_before,
|
80
|
+
ended_after,
|
81
|
+
ended_before,
|
82
|
+
]
|
83
|
+
)
|
84
|
+
|
85
|
+
if has_identifiers and has_filters:
|
86
|
+
click.echo(
|
87
|
+
"Error: Cannot use both experiment identifiers and filter options. Choose one approach.",
|
88
|
+
err=True,
|
89
|
+
)
|
90
|
+
ctx.exit(1)
|
91
|
+
|
92
|
+
if not has_identifiers and not has_filters:
|
93
|
+
click.echo(
|
94
|
+
"Error: Must specify either experiment identifiers or filter options",
|
95
|
+
err=True,
|
96
|
+
)
|
97
|
+
ctx.exit(1)
|
98
|
+
|
99
|
+
# Parse time specifications
|
100
|
+
started_after_dt = parse_time_spec(started_after) if started_after else None
|
101
|
+
started_before_dt = parse_time_spec(started_before) if started_before else None
|
102
|
+
ended_after_dt = parse_time_spec(ended_after) if ended_after else None
|
103
|
+
ended_before_dt = parse_time_spec(ended_before) if ended_before else None
|
104
|
+
|
105
|
+
# Find experiments to archive
|
106
|
+
if experiment_identifiers:
|
107
|
+
# Archive specific experiments by ID/name
|
108
|
+
experiments = find_experiments_by_identifiers(
|
109
|
+
filter_obj,
|
110
|
+
list(experiment_identifiers),
|
111
|
+
include_archived=False, # Can't archive already archived experiments
|
112
|
+
)
|
113
|
+
else:
|
114
|
+
# Archive experiments by filter criteria
|
115
|
+
|
116
|
+
experiments = find_experiments_by_filters(
|
117
|
+
filter_obj,
|
118
|
+
status=status,
|
119
|
+
name_pattern=name_pattern,
|
120
|
+
tags=list(tags) if tags else None,
|
121
|
+
started_after=started_after_dt,
|
122
|
+
started_before=started_before_dt,
|
123
|
+
ended_after=ended_after_dt,
|
124
|
+
ended_before=ended_before_dt,
|
125
|
+
include_archived=False, # Can't archive already archived experiments
|
126
|
+
)
|
127
|
+
|
128
|
+
# Filter out already archived experiments (extra safety)
|
129
|
+
experiments = [exp for exp in experiments if not exp.get("archived", False)]
|
130
|
+
|
131
|
+
if not experiments:
|
132
|
+
click.echo("No experiments found to archive.")
|
133
|
+
return
|
134
|
+
|
135
|
+
# Show experiments and get confirmation
|
136
|
+
if not confirm_experiment_operation(
|
137
|
+
experiments, "archive", force, default_yes=True
|
138
|
+
):
|
139
|
+
click.echo("Archive operation cancelled.")
|
140
|
+
return
|
141
|
+
|
142
|
+
# Archive experiments
|
143
|
+
click.echo(f"Archiving {len(experiments)} experiment(s)...")
|
144
|
+
|
145
|
+
success_count = 0
|
146
|
+
for exp in experiments:
|
147
|
+
try:
|
148
|
+
experiment_id = exp["id"]
|
149
|
+
filter_obj.manager.storage.archive_experiment(experiment_id)
|
150
|
+
|
151
|
+
# Show progress
|
152
|
+
exp_name = exp.get("name", "[unnamed]")
|
153
|
+
click.echo(f" ✓ Archived {experiment_id} ({exp_name})")
|
154
|
+
success_count += 1
|
155
|
+
|
156
|
+
except Exception as e:
|
157
|
+
exp_name = exp.get("name", "[unnamed]")
|
158
|
+
click.echo(
|
159
|
+
f" ✗ Failed to archive {experiment_id} ({exp_name}): {e}", err=True
|
160
|
+
)
|
161
|
+
|
162
|
+
# Summary
|
163
|
+
if success_count == len(experiments):
|
164
|
+
click.echo(f"Successfully archived {success_count} experiment(s).")
|
165
|
+
else:
|
166
|
+
failed_count = len(experiments) - success_count
|
167
|
+
click.echo(
|
168
|
+
f"Archived {success_count} experiment(s), {failed_count} failed.",
|
169
|
+
err=True,
|
170
|
+
)
|
171
|
+
ctx.exit(1)
|
172
|
+
|
173
|
+
except click.ClickException:
|
174
|
+
raise # Re-raise ClickException to show proper error message
|
175
|
+
except Exception as e:
|
176
|
+
click.echo(f"Error: {e}", err=True)
|
177
|
+
ctx.exit(1)
|
@@ -0,0 +1,320 @@
|
|
1
|
+
"""
|
2
|
+
Compare experiments - interactive table with parameters and metrics.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
import click
|
8
|
+
|
9
|
+
from ...core.comparison import ExperimentComparisonData
|
10
|
+
from ...core.constants import EXPERIMENT_STATUSES
|
11
|
+
from ...ui.compare_table import run_comparison_table
|
12
|
+
from ..filters import ExperimentFilter, parse_time_spec
|
13
|
+
from .confirm import find_experiments_by_filters, find_experiments_by_identifiers
|
14
|
+
|
15
|
+
|
16
|
+
@click.command("compare")
|
17
|
+
@click.argument("experiment_identifiers", nargs=-1)
|
18
|
+
@click.option(
|
19
|
+
"--status",
|
20
|
+
type=click.Choice(EXPERIMENT_STATUSES),
|
21
|
+
help="Compare experiments with specific status",
|
22
|
+
)
|
23
|
+
@click.option(
|
24
|
+
"--name",
|
25
|
+
"name_pattern",
|
26
|
+
help="Compare experiments matching name pattern (glob syntax)",
|
27
|
+
)
|
28
|
+
@click.option(
|
29
|
+
"--tag", "tags", multiple=True, help="Compare experiments with ALL specified tags"
|
30
|
+
)
|
31
|
+
@click.option(
|
32
|
+
"--started-after",
|
33
|
+
help="Compare experiments started after date/time (e.g., '2025-01-01', 'yesterday', '1 week ago')",
|
34
|
+
)
|
35
|
+
@click.option("--started-before", help="Compare experiments started before date/time")
|
36
|
+
@click.option("--ended-after", help="Compare experiments ended after date/time")
|
37
|
+
@click.option("--ended-before", help="Compare experiments ended before date/time")
|
38
|
+
@click.option(
|
39
|
+
"--archived",
|
40
|
+
is_flag=True,
|
41
|
+
help="Include archived experiments (default: only regular experiments)",
|
42
|
+
)
|
43
|
+
@click.option(
|
44
|
+
"--params",
|
45
|
+
help="Show only specified parameters (comma-separated, e.g., 'learning_rate,epochs')",
|
46
|
+
)
|
47
|
+
@click.option(
|
48
|
+
"--metrics",
|
49
|
+
help="Show only specified metrics (comma-separated, e.g., 'accuracy,loss')",
|
50
|
+
)
|
51
|
+
@click.option(
|
52
|
+
"--only-different",
|
53
|
+
is_flag=True,
|
54
|
+
help="Show only columns where values differ between experiments",
|
55
|
+
)
|
56
|
+
@click.option(
|
57
|
+
"--export",
|
58
|
+
"export_path",
|
59
|
+
help="Export comparison data to CSV file instead of interactive view",
|
60
|
+
)
|
61
|
+
@click.option(
|
62
|
+
"--no-interactive",
|
63
|
+
is_flag=True,
|
64
|
+
help="Print static table instead of interactive view",
|
65
|
+
)
|
66
|
+
@click.option(
|
67
|
+
"--max-rows",
|
68
|
+
type=int,
|
69
|
+
help="Limit number of experiments displayed",
|
70
|
+
)
|
71
|
+
@click.pass_context
|
72
|
+
def compare_experiments(
|
73
|
+
ctx,
|
74
|
+
experiment_identifiers: tuple,
|
75
|
+
status: Optional[str],
|
76
|
+
name_pattern: Optional[str],
|
77
|
+
tags: tuple,
|
78
|
+
started_after: Optional[str],
|
79
|
+
started_before: Optional[str],
|
80
|
+
ended_after: Optional[str],
|
81
|
+
ended_before: Optional[str],
|
82
|
+
archived: bool,
|
83
|
+
params: Optional[str],
|
84
|
+
metrics: Optional[str],
|
85
|
+
only_different: bool,
|
86
|
+
export_path: Optional[str],
|
87
|
+
no_interactive: bool,
|
88
|
+
max_rows: Optional[int],
|
89
|
+
):
|
90
|
+
"""
|
91
|
+
Compare experiments in an interactive table showing parameters and metrics.
|
92
|
+
|
93
|
+
EXPERIMENT_IDENTIFIERS can be experiment IDs or names.
|
94
|
+
If no identifiers provided, experiments are filtered by options.
|
95
|
+
|
96
|
+
The interactive table supports:
|
97
|
+
- Navigation with arrow keys or hjkl
|
98
|
+
- Sorting by any column (s/S for asc/desc, 1/2 for numeric)
|
99
|
+
- Export to CSV (press 'e' in interactive mode)
|
100
|
+
- Help (press '?' for keyboard shortcuts)
|
101
|
+
|
102
|
+
Examples:
|
103
|
+
\\b
|
104
|
+
yanex compare # All experiments
|
105
|
+
yanex compare exp1 exp2 exp3 # Specific experiments
|
106
|
+
yanex compare --status completed # Completed experiments
|
107
|
+
yanex compare --tag training --only-different # Training experiments, show differences only
|
108
|
+
yanex compare --params learning_rate,epochs # Show only specified parameters
|
109
|
+
yanex compare --export results.csv # Export to CSV
|
110
|
+
yanex compare --no-interactive # Static table output
|
111
|
+
"""
|
112
|
+
try:
|
113
|
+
filter_obj = ExperimentFilter()
|
114
|
+
|
115
|
+
# Validate mutually exclusive targeting
|
116
|
+
has_identifiers = len(experiment_identifiers) > 0
|
117
|
+
has_filters = any(
|
118
|
+
[
|
119
|
+
status,
|
120
|
+
name_pattern,
|
121
|
+
tags,
|
122
|
+
started_after,
|
123
|
+
started_before,
|
124
|
+
ended_after,
|
125
|
+
ended_before,
|
126
|
+
]
|
127
|
+
)
|
128
|
+
|
129
|
+
if has_identifiers and has_filters:
|
130
|
+
click.echo(
|
131
|
+
"Error: Cannot use both experiment identifiers and filter options. Choose one approach.",
|
132
|
+
err=True,
|
133
|
+
)
|
134
|
+
ctx.exit(1)
|
135
|
+
|
136
|
+
if not has_identifiers and not has_filters:
|
137
|
+
# No filters specified - use all experiments
|
138
|
+
pass
|
139
|
+
|
140
|
+
# Parse time specifications
|
141
|
+
started_after_dt = parse_time_spec(started_after) if started_after else None
|
142
|
+
started_before_dt = parse_time_spec(started_before) if started_before else None
|
143
|
+
ended_after_dt = parse_time_spec(ended_after) if ended_after else None
|
144
|
+
ended_before_dt = parse_time_spec(ended_before) if ended_before else None
|
145
|
+
|
146
|
+
# Find experiments to compare
|
147
|
+
if experiment_identifiers:
|
148
|
+
# Compare specific experiments by ID/name
|
149
|
+
experiments = find_experiments_by_identifiers(
|
150
|
+
filter_obj, list(experiment_identifiers), include_archived=archived
|
151
|
+
)
|
152
|
+
else:
|
153
|
+
# Compare experiments by filter criteria
|
154
|
+
experiments = find_experiments_by_filters(
|
155
|
+
filter_obj,
|
156
|
+
status=status,
|
157
|
+
name_pattern=name_pattern,
|
158
|
+
tags=list(tags) if tags else None,
|
159
|
+
started_after=started_after_dt,
|
160
|
+
started_before=started_before_dt,
|
161
|
+
ended_after=ended_after_dt,
|
162
|
+
ended_before=ended_before_dt,
|
163
|
+
include_archived=archived,
|
164
|
+
)
|
165
|
+
|
166
|
+
if not experiments:
|
167
|
+
location = "archived" if archived else "regular"
|
168
|
+
click.echo(f"No {location} experiments found to compare.")
|
169
|
+
return
|
170
|
+
|
171
|
+
# Limit number of experiments if requested
|
172
|
+
if max_rows and len(experiments) > max_rows:
|
173
|
+
experiments = experiments[:max_rows]
|
174
|
+
click.echo(f"Limiting display to first {max_rows} experiments.")
|
175
|
+
|
176
|
+
# Extract experiment IDs from the experiment metadata dictionaries
|
177
|
+
experiment_ids = [exp["id"] for exp in experiments]
|
178
|
+
|
179
|
+
# Parse parameter and metric lists
|
180
|
+
param_list = None
|
181
|
+
if params:
|
182
|
+
param_list = [p.strip() for p in params.split(",") if p.strip()]
|
183
|
+
|
184
|
+
metric_list = None
|
185
|
+
if metrics:
|
186
|
+
metric_list = [m.strip() for m in metrics.split(",") if m.strip()]
|
187
|
+
|
188
|
+
# Get comparison data
|
189
|
+
comparison_data_extractor = ExperimentComparisonData()
|
190
|
+
comparison_data = comparison_data_extractor.get_comparison_data(
|
191
|
+
experiment_ids=experiment_ids,
|
192
|
+
params=param_list,
|
193
|
+
metrics=metric_list,
|
194
|
+
only_different=only_different,
|
195
|
+
include_archived=archived,
|
196
|
+
)
|
197
|
+
|
198
|
+
if not comparison_data.get("rows"):
|
199
|
+
click.echo("No comparison data available.")
|
200
|
+
return
|
201
|
+
|
202
|
+
# Handle export mode
|
203
|
+
if export_path:
|
204
|
+
_export_comparison_data(comparison_data, export_path)
|
205
|
+
click.echo(f"Comparison data exported to {export_path}")
|
206
|
+
return
|
207
|
+
|
208
|
+
# Handle static table mode
|
209
|
+
if no_interactive:
|
210
|
+
_print_static_table(comparison_data)
|
211
|
+
return
|
212
|
+
|
213
|
+
# Run interactive table
|
214
|
+
total_experiments = comparison_data.get("total_experiments", 0)
|
215
|
+
param_count = len(comparison_data.get("param_columns", []))
|
216
|
+
metric_count = len(comparison_data.get("metric_columns", []))
|
217
|
+
|
218
|
+
title = f"yanex compare - {total_experiments} experiments, {param_count} params, {metric_count} metrics"
|
219
|
+
|
220
|
+
# Set default export path
|
221
|
+
default_export = export_path or "yanex_comparison.csv"
|
222
|
+
|
223
|
+
run_comparison_table(comparison_data, title=title, export_path=default_export)
|
224
|
+
|
225
|
+
except click.ClickException:
|
226
|
+
raise # Re-raise ClickException to show proper error message
|
227
|
+
except Exception as e:
|
228
|
+
click.echo(f"Error: {e}", err=True)
|
229
|
+
ctx.exit(1)
|
230
|
+
|
231
|
+
|
232
|
+
def _export_comparison_data(comparison_data: dict, file_path: str) -> None:
|
233
|
+
"""Export comparison data to CSV file."""
|
234
|
+
import csv
|
235
|
+
from pathlib import Path
|
236
|
+
|
237
|
+
rows = comparison_data.get("rows", [])
|
238
|
+
if not rows:
|
239
|
+
raise ValueError("No data to export")
|
240
|
+
|
241
|
+
# Get column order - use same order as UI
|
242
|
+
first_row = rows[0]
|
243
|
+
column_keys = list(first_row.keys())
|
244
|
+
|
245
|
+
# Generate headers
|
246
|
+
column_headers = []
|
247
|
+
for key in column_keys:
|
248
|
+
if key.startswith("param:"):
|
249
|
+
column_headers.append(f"param_{key[6:]}") # Remove emoji for CSV
|
250
|
+
elif key.startswith("metric:"):
|
251
|
+
column_headers.append(f"metric_{key[7:]}") # Remove emoji for CSV
|
252
|
+
else:
|
253
|
+
column_headers.append(key)
|
254
|
+
|
255
|
+
# Write CSV
|
256
|
+
path = Path(file_path)
|
257
|
+
with path.open("w", newline="", encoding="utf-8") as csvfile:
|
258
|
+
writer = csv.writer(csvfile)
|
259
|
+
|
260
|
+
# Write header
|
261
|
+
writer.writerow(column_headers)
|
262
|
+
|
263
|
+
# Write data rows
|
264
|
+
for row_data in rows:
|
265
|
+
row_values = [row_data.get(key, "-") for key in column_keys]
|
266
|
+
writer.writerow(row_values)
|
267
|
+
|
268
|
+
|
269
|
+
def _print_static_table(comparison_data: dict) -> None:
|
270
|
+
"""Print comparison data as a static table."""
|
271
|
+
from rich.console import Console
|
272
|
+
from rich.table import Table
|
273
|
+
|
274
|
+
console = Console()
|
275
|
+
rows = comparison_data.get("rows", [])
|
276
|
+
|
277
|
+
if not rows:
|
278
|
+
console.print("No data to display")
|
279
|
+
return
|
280
|
+
|
281
|
+
# Create table
|
282
|
+
table = Table(show_header=True, header_style="bold magenta")
|
283
|
+
|
284
|
+
# Get column order
|
285
|
+
first_row = rows[0]
|
286
|
+
column_keys = list(first_row.keys())
|
287
|
+
|
288
|
+
# Add columns with formatted headers
|
289
|
+
for key in column_keys:
|
290
|
+
if key.startswith("param:"):
|
291
|
+
header = f"📊 {key[6:]}"
|
292
|
+
elif key.startswith("metric:"):
|
293
|
+
header = f"📈 {key[7:]}"
|
294
|
+
elif key == "duration":
|
295
|
+
header = "Duration"
|
296
|
+
elif key == "tags":
|
297
|
+
header = "Tags"
|
298
|
+
elif key == "id":
|
299
|
+
header = "ID"
|
300
|
+
elif key == "name":
|
301
|
+
header = "Name"
|
302
|
+
else:
|
303
|
+
header = key.title()
|
304
|
+
table.add_column(header)
|
305
|
+
|
306
|
+
# Add rows
|
307
|
+
for row_data in rows:
|
308
|
+
row_values = [str(row_data.get(key, "-")) for key in column_keys]
|
309
|
+
table.add_row(*row_values)
|
310
|
+
|
311
|
+
# Print table
|
312
|
+
console.print(table)
|
313
|
+
|
314
|
+
# Print summary
|
315
|
+
param_count = len(comparison_data.get("param_columns", []))
|
316
|
+
metric_count = len(comparison_data.get("metric_columns", []))
|
317
|
+
|
318
|
+
console.print(
|
319
|
+
f"\n[dim]Showing {len(rows)} experiments with {param_count} parameters and {metric_count} metrics[/dim]"
|
320
|
+
)
|
@@ -0,0 +1,198 @@
|
|
1
|
+
"""
|
2
|
+
Confirmation utilities for bulk operations on experiments.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Dict, List
|
6
|
+
|
7
|
+
import click
|
8
|
+
|
9
|
+
from ..filters import ExperimentFilter
|
10
|
+
from ..formatters.console import ExperimentTableFormatter
|
11
|
+
|
12
|
+
|
13
|
+
def confirm_experiment_operation(
|
14
|
+
experiments: List[Dict[str, Any]],
|
15
|
+
operation: str,
|
16
|
+
force: bool = False,
|
17
|
+
operation_verb: str = None,
|
18
|
+
default_yes: bool = False,
|
19
|
+
) -> bool:
|
20
|
+
"""
|
21
|
+
Show experiments and confirm operation.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
experiments: List of experiment dictionaries to show
|
25
|
+
operation: Operation name (e.g., "archive", "delete")
|
26
|
+
force: Skip confirmation if True
|
27
|
+
operation_verb: Past tense verb for confirmation (e.g., "archived", "deleted")
|
28
|
+
default_yes: If True, default to Yes instead of No
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
True if user confirms, False otherwise
|
32
|
+
"""
|
33
|
+
if not experiments:
|
34
|
+
click.echo(f"No experiments found to {operation}.")
|
35
|
+
return False
|
36
|
+
|
37
|
+
if operation_verb is None:
|
38
|
+
operation_verb = f"{operation}d" # Default: "archive" -> "archived"
|
39
|
+
|
40
|
+
# Display table of experiments
|
41
|
+
formatter = ExperimentTableFormatter()
|
42
|
+
|
43
|
+
click.echo(
|
44
|
+
f"The following {len(experiments)} experiment(s) will be {operation_verb}:"
|
45
|
+
)
|
46
|
+
click.echo()
|
47
|
+
|
48
|
+
formatter.print_experiments_table(experiments)
|
49
|
+
click.echo()
|
50
|
+
|
51
|
+
# Skip confirmation if force flag is set
|
52
|
+
if force:
|
53
|
+
return True
|
54
|
+
|
55
|
+
# Get confirmation from user
|
56
|
+
if len(experiments) == 1:
|
57
|
+
message = f"{operation.capitalize()} this experiment?"
|
58
|
+
else:
|
59
|
+
message = f"{operation.capitalize()} these {len(experiments)} experiments?"
|
60
|
+
|
61
|
+
return click.confirm(message, default=default_yes)
|
62
|
+
|
63
|
+
|
64
|
+
def find_experiments_by_identifiers(
|
65
|
+
filter_obj: ExperimentFilter,
|
66
|
+
identifiers: List[str],
|
67
|
+
include_archived: bool = False,
|
68
|
+
archived_only: bool = False,
|
69
|
+
) -> List[Dict[str, Any]]:
|
70
|
+
"""
|
71
|
+
Find experiments by list of identifiers (IDs or names).
|
72
|
+
|
73
|
+
Args:
|
74
|
+
filter_obj: ExperimentFilter instance
|
75
|
+
identifiers: List of experiment IDs or names
|
76
|
+
include_archived: Whether to search archived experiments
|
77
|
+
archived_only: If True, search ONLY archived experiments (for unarchive command)
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
List of found experiments
|
81
|
+
|
82
|
+
Raises:
|
83
|
+
click.ClickException: If any identifier is not found or ambiguous
|
84
|
+
"""
|
85
|
+
from .show import find_experiment # Import here to avoid circular import
|
86
|
+
|
87
|
+
all_experiments = []
|
88
|
+
|
89
|
+
for identifier in identifiers:
|
90
|
+
if archived_only:
|
91
|
+
# For unarchive: search all experiments but only consider archived ones
|
92
|
+
result = find_experiment(filter_obj, identifier, include_archived=True)
|
93
|
+
|
94
|
+
if result is None:
|
95
|
+
raise click.ClickException(
|
96
|
+
f"No archived experiment found with ID or name '{identifier}'"
|
97
|
+
)
|
98
|
+
|
99
|
+
if isinstance(result, list):
|
100
|
+
# Filter to only archived experiments
|
101
|
+
archived_results = [exp for exp in result if exp.get("archived", False)]
|
102
|
+
|
103
|
+
if not archived_results:
|
104
|
+
raise click.ClickException(
|
105
|
+
f"No archived experiment found with name '{identifier}'"
|
106
|
+
)
|
107
|
+
elif len(archived_results) == 1:
|
108
|
+
result = archived_results[0]
|
109
|
+
else:
|
110
|
+
# Multiple archived experiments with same name
|
111
|
+
click.echo(
|
112
|
+
f"Multiple archived experiments found with name '{identifier}':"
|
113
|
+
)
|
114
|
+
click.echo()
|
115
|
+
|
116
|
+
formatter = ExperimentTableFormatter()
|
117
|
+
formatter.print_experiments_table(archived_results)
|
118
|
+
click.echo()
|
119
|
+
click.echo(
|
120
|
+
"Please use specific experiment IDs instead of names for bulk operations."
|
121
|
+
)
|
122
|
+
raise click.ClickException(
|
123
|
+
f"Ambiguous archived experiment name: '{identifier}'"
|
124
|
+
)
|
125
|
+
else:
|
126
|
+
# Single result - check if it's archived
|
127
|
+
if not result.get("archived", False):
|
128
|
+
raise click.ClickException(
|
129
|
+
f"Experiment '{identifier}' is not archived"
|
130
|
+
)
|
131
|
+
else:
|
132
|
+
# Normal mode: search as specified
|
133
|
+
result = find_experiment(filter_obj, identifier, include_archived)
|
134
|
+
|
135
|
+
if result is None:
|
136
|
+
location = "archived" if include_archived else "regular"
|
137
|
+
raise click.ClickException(
|
138
|
+
f"No {location} experiment found with ID or name '{identifier}'"
|
139
|
+
)
|
140
|
+
|
141
|
+
if isinstance(result, list):
|
142
|
+
# Multiple experiments with same name
|
143
|
+
click.echo(f"Multiple experiments found with name '{identifier}':")
|
144
|
+
click.echo()
|
145
|
+
|
146
|
+
formatter = ExperimentTableFormatter()
|
147
|
+
formatter.print_experiments_table(result)
|
148
|
+
click.echo()
|
149
|
+
click.echo(
|
150
|
+
"Please use specific experiment IDs instead of names for bulk operations."
|
151
|
+
)
|
152
|
+
raise click.ClickException(f"Ambiguous experiment name: '{identifier}'")
|
153
|
+
|
154
|
+
# Single experiment found
|
155
|
+
all_experiments.append(result)
|
156
|
+
|
157
|
+
return all_experiments
|
158
|
+
|
159
|
+
|
160
|
+
def find_experiments_by_filters(
|
161
|
+
filter_obj: ExperimentFilter,
|
162
|
+
status: str = None,
|
163
|
+
name_pattern: str = None,
|
164
|
+
tags: List[str] = None,
|
165
|
+
started_after=None,
|
166
|
+
started_before=None,
|
167
|
+
ended_after=None,
|
168
|
+
ended_before=None,
|
169
|
+
include_archived: bool = False,
|
170
|
+
) -> List[Dict[str, Any]]:
|
171
|
+
"""
|
172
|
+
Find experiments using filter criteria.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
filter_obj: ExperimentFilter instance
|
176
|
+
status: Filter by status
|
177
|
+
name_pattern: Filter by name pattern
|
178
|
+
tags: Filter by tags
|
179
|
+
started_after: Filter by start time
|
180
|
+
started_before: Filter by start time
|
181
|
+
ended_after: Filter by end time
|
182
|
+
ended_before: Filter by end time
|
183
|
+
include_archived: Whether to search archived experiments
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
List of found experiments
|
187
|
+
"""
|
188
|
+
return filter_obj.filter_experiments(
|
189
|
+
status=status,
|
190
|
+
name_pattern=name_pattern,
|
191
|
+
tags=tags,
|
192
|
+
started_after=started_after,
|
193
|
+
started_before=started_before,
|
194
|
+
ended_after=ended_after,
|
195
|
+
ended_before=ended_before,
|
196
|
+
include_all=True, # Get all matching experiments for bulk operations
|
197
|
+
include_archived=include_archived,
|
198
|
+
)
|