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,203 @@
|
|
1
|
+
"""
|
2
|
+
Delete experiments permanently.
|
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("delete")
|
19
|
+
@click.argument("experiment_identifiers", nargs=-1)
|
20
|
+
@click.option(
|
21
|
+
"--status",
|
22
|
+
type=click.Choice(EXPERIMENT_STATUSES),
|
23
|
+
help="Delete experiments with specific status",
|
24
|
+
)
|
25
|
+
@click.option(
|
26
|
+
"--name",
|
27
|
+
"name_pattern",
|
28
|
+
help="Delete experiments matching name pattern (glob syntax)",
|
29
|
+
)
|
30
|
+
@click.option(
|
31
|
+
"--tag", "tags", multiple=True, help="Delete experiments with ALL specified tags"
|
32
|
+
)
|
33
|
+
@click.option(
|
34
|
+
"--started-after",
|
35
|
+
help="Delete experiments started after date/time (e.g., '2025-01-01', 'yesterday', '1 week ago')",
|
36
|
+
)
|
37
|
+
@click.option("--started-before", help="Delete experiments started before date/time")
|
38
|
+
@click.option("--ended-after", help="Delete experiments ended after date/time")
|
39
|
+
@click.option("--ended-before", help="Delete experiments ended before date/time")
|
40
|
+
@click.option(
|
41
|
+
"--archived",
|
42
|
+
is_flag=True,
|
43
|
+
help="Delete from archived experiments (default: delete from regular experiments)",
|
44
|
+
)
|
45
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
46
|
+
@click.pass_context
|
47
|
+
def delete_experiments(
|
48
|
+
ctx,
|
49
|
+
experiment_identifiers: tuple,
|
50
|
+
status: Optional[str],
|
51
|
+
name_pattern: Optional[str],
|
52
|
+
tags: tuple,
|
53
|
+
started_after: Optional[str],
|
54
|
+
started_before: Optional[str],
|
55
|
+
ended_after: Optional[str],
|
56
|
+
ended_before: Optional[str],
|
57
|
+
archived: bool,
|
58
|
+
force: bool,
|
59
|
+
):
|
60
|
+
"""
|
61
|
+
Permanently delete experiments.
|
62
|
+
|
63
|
+
⚠️ WARNING: This operation cannot be undone!
|
64
|
+
|
65
|
+
EXPERIMENT_IDENTIFIERS can be experiment IDs or names.
|
66
|
+
If no identifiers provided, experiments are filtered by options.
|
67
|
+
|
68
|
+
Examples:
|
69
|
+
\\b
|
70
|
+
yanex delete exp1 exp2 # Delete specific experiments
|
71
|
+
yanex delete --status failed # Delete all failed experiments
|
72
|
+
yanex delete --archived --ended-before "6 months ago"
|
73
|
+
yanex delete --name "*test*" # Delete experiments with "test" in name
|
74
|
+
yanex delete --tag temp # Delete experiments with "temp" tag
|
75
|
+
"""
|
76
|
+
try:
|
77
|
+
filter_obj = ExperimentFilter()
|
78
|
+
|
79
|
+
# Validate mutually exclusive targeting
|
80
|
+
has_identifiers = len(experiment_identifiers) > 0
|
81
|
+
has_filters = any(
|
82
|
+
[
|
83
|
+
status,
|
84
|
+
name_pattern,
|
85
|
+
tags,
|
86
|
+
started_after,
|
87
|
+
started_before,
|
88
|
+
ended_after,
|
89
|
+
ended_before,
|
90
|
+
]
|
91
|
+
)
|
92
|
+
|
93
|
+
if has_identifiers and has_filters:
|
94
|
+
click.echo(
|
95
|
+
"Error: Cannot use both experiment identifiers and filter options. Choose one approach.",
|
96
|
+
err=True,
|
97
|
+
)
|
98
|
+
ctx.exit(1)
|
99
|
+
|
100
|
+
if not has_identifiers and not has_filters:
|
101
|
+
click.echo(
|
102
|
+
"Error: Must specify either experiment identifiers or filter options",
|
103
|
+
err=True,
|
104
|
+
)
|
105
|
+
ctx.exit(1)
|
106
|
+
|
107
|
+
# Parse time specifications
|
108
|
+
started_after_dt = parse_time_spec(started_after) if started_after else None
|
109
|
+
started_before_dt = parse_time_spec(started_before) if started_before else None
|
110
|
+
ended_after_dt = parse_time_spec(ended_after) if ended_after else None
|
111
|
+
ended_before_dt = parse_time_spec(ended_before) if ended_before else None
|
112
|
+
|
113
|
+
# Find experiments to delete
|
114
|
+
if experiment_identifiers:
|
115
|
+
# Delete specific experiments by ID/name
|
116
|
+
experiments = find_experiments_by_identifiers(
|
117
|
+
filter_obj, list(experiment_identifiers), include_archived=archived
|
118
|
+
)
|
119
|
+
else:
|
120
|
+
# Delete experiments by filter criteria
|
121
|
+
|
122
|
+
experiments = find_experiments_by_filters(
|
123
|
+
filter_obj,
|
124
|
+
status=status,
|
125
|
+
name_pattern=name_pattern,
|
126
|
+
tags=list(tags) if tags else None,
|
127
|
+
started_after=started_after_dt,
|
128
|
+
started_before=started_before_dt,
|
129
|
+
ended_after=ended_after_dt,
|
130
|
+
ended_before=ended_before_dt,
|
131
|
+
include_archived=archived,
|
132
|
+
)
|
133
|
+
|
134
|
+
# Filter experiments based on archived flag
|
135
|
+
if archived:
|
136
|
+
experiments = [exp for exp in experiments if exp.get("archived", False)]
|
137
|
+
else:
|
138
|
+
experiments = [exp for exp in experiments if not exp.get("archived", False)]
|
139
|
+
|
140
|
+
if not experiments:
|
141
|
+
location = "archived" if archived else "regular"
|
142
|
+
click.echo(f"No {location} experiments found to delete.")
|
143
|
+
return
|
144
|
+
|
145
|
+
# Show experiments and get confirmation (always required for deletion)
|
146
|
+
operation_verb = "permanently deleted"
|
147
|
+
if not confirm_experiment_operation(
|
148
|
+
experiments, "delete", force, operation_verb
|
149
|
+
):
|
150
|
+
click.echo("Delete operation cancelled.")
|
151
|
+
return
|
152
|
+
|
153
|
+
# Additional warning for bulk deletions
|
154
|
+
if len(experiments) > 1 and not force:
|
155
|
+
click.echo()
|
156
|
+
click.echo("⚠️ You are about to permanently delete multiple experiments.")
|
157
|
+
click.echo(" This action cannot be undone!")
|
158
|
+
if not click.confirm("Are you absolutely sure?", default=False):
|
159
|
+
click.echo("Delete operation cancelled.")
|
160
|
+
return
|
161
|
+
|
162
|
+
# Delete experiments
|
163
|
+
click.echo(f"Deleting {len(experiments)} experiment(s)...")
|
164
|
+
|
165
|
+
success_count = 0
|
166
|
+
for exp in experiments:
|
167
|
+
try:
|
168
|
+
experiment_id = exp["id"]
|
169
|
+
|
170
|
+
if exp.get("archived", False):
|
171
|
+
# Delete from archived directory
|
172
|
+
filter_obj.manager.storage.delete_archived_experiment(experiment_id)
|
173
|
+
else:
|
174
|
+
# Delete from regular directory
|
175
|
+
filter_obj.manager.storage.delete_experiment(experiment_id)
|
176
|
+
|
177
|
+
# Show progress
|
178
|
+
exp_name = exp.get("name", "[unnamed]")
|
179
|
+
click.echo(f" ✓ Deleted {experiment_id} ({exp_name})")
|
180
|
+
success_count += 1
|
181
|
+
|
182
|
+
except Exception as e:
|
183
|
+
exp_name = exp.get("name", "[unnamed]")
|
184
|
+
click.echo(
|
185
|
+
f" ✗ Failed to delete {experiment_id} ({exp_name}): {e}", err=True
|
186
|
+
)
|
187
|
+
|
188
|
+
# Summary
|
189
|
+
if success_count == len(experiments):
|
190
|
+
click.echo(f"Successfully deleted {success_count} experiment(s).")
|
191
|
+
else:
|
192
|
+
failed_count = len(experiments) - success_count
|
193
|
+
click.echo(
|
194
|
+
f"Deleted {success_count} experiment(s), {failed_count} failed.",
|
195
|
+
err=True,
|
196
|
+
)
|
197
|
+
ctx.exit(1)
|
198
|
+
|
199
|
+
except click.ClickException:
|
200
|
+
raise # Re-raise ClickException to show proper error message
|
201
|
+
except Exception as e:
|
202
|
+
click.echo(f"Error: {e}", err=True)
|
203
|
+
ctx.exit(1)
|
@@ -0,0 +1,243 @@
|
|
1
|
+
"""
|
2
|
+
List command implementation for yanex CLI.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import List, Optional
|
6
|
+
|
7
|
+
import click
|
8
|
+
|
9
|
+
from ...core.constants import EXPERIMENT_STATUSES
|
10
|
+
from ..filters import ExperimentFilter, parse_time_spec
|
11
|
+
from ..formatters import ExperimentTableFormatter
|
12
|
+
|
13
|
+
|
14
|
+
@click.command()
|
15
|
+
@click.option(
|
16
|
+
"--all",
|
17
|
+
"show_all",
|
18
|
+
is_flag=True,
|
19
|
+
help="Show all experiments (overrides default limit of 10)",
|
20
|
+
)
|
21
|
+
@click.option("-n", "--limit", type=int, help="Maximum number of experiments to show")
|
22
|
+
@click.option(
|
23
|
+
"--status",
|
24
|
+
type=click.Choice(EXPERIMENT_STATUSES, case_sensitive=False),
|
25
|
+
help="Filter by experiment status",
|
26
|
+
)
|
27
|
+
@click.option(
|
28
|
+
"--name",
|
29
|
+
"name_pattern",
|
30
|
+
help="Filter by name using glob patterns (e.g., '*tuning*')",
|
31
|
+
)
|
32
|
+
@click.option(
|
33
|
+
"--tag",
|
34
|
+
"tags",
|
35
|
+
multiple=True,
|
36
|
+
help="Filter by tag (repeatable, experiments must have ALL specified tags)",
|
37
|
+
)
|
38
|
+
@click.option(
|
39
|
+
"--started",
|
40
|
+
"started_spec",
|
41
|
+
help="Show experiments started on or after this time (e.g., 'today', 'last week', '2023-01-01')",
|
42
|
+
)
|
43
|
+
@click.option(
|
44
|
+
"--ended",
|
45
|
+
"ended_spec",
|
46
|
+
help="Show experiments ended on or after this time (e.g., 'today', 'last week', '2023-01-01')",
|
47
|
+
)
|
48
|
+
@click.option(
|
49
|
+
"--archived",
|
50
|
+
is_flag=True,
|
51
|
+
help="Show archived experiments instead of regular experiments",
|
52
|
+
)
|
53
|
+
@click.pass_context
|
54
|
+
def list_experiments(
|
55
|
+
ctx: click.Context,
|
56
|
+
show_all: bool,
|
57
|
+
limit: Optional[int],
|
58
|
+
status: Optional[str],
|
59
|
+
name_pattern: Optional[str],
|
60
|
+
tags: List[str],
|
61
|
+
started_spec: Optional[str],
|
62
|
+
ended_spec: Optional[str],
|
63
|
+
archived: bool,
|
64
|
+
) -> None:
|
65
|
+
"""
|
66
|
+
List experiments with filtering options.
|
67
|
+
|
68
|
+
Shows the last 10 experiments by default. Use --all to show all experiments
|
69
|
+
or -n to specify a custom limit.
|
70
|
+
|
71
|
+
Examples:
|
72
|
+
|
73
|
+
# Show last 10 experiments
|
74
|
+
yanex list
|
75
|
+
|
76
|
+
# Show all experiments
|
77
|
+
yanex list --all
|
78
|
+
|
79
|
+
# Show last 5 experiments
|
80
|
+
yanex list -n 5
|
81
|
+
|
82
|
+
# Filter by status
|
83
|
+
yanex list --status completed
|
84
|
+
|
85
|
+
# Filter by name pattern
|
86
|
+
yanex list --name "*tuning*"
|
87
|
+
|
88
|
+
# Filter by multiple tags (AND logic)
|
89
|
+
yanex list --tag hyperopt --tag production
|
90
|
+
|
91
|
+
# Filter by time (started since last week)
|
92
|
+
yanex list --started "last week"
|
93
|
+
|
94
|
+
# Complex filtering
|
95
|
+
yanex list --status completed --tag production --started "last month" -n 20
|
96
|
+
"""
|
97
|
+
verbose = ctx.obj.get("verbose", False)
|
98
|
+
|
99
|
+
try:
|
100
|
+
# Parse time specifications
|
101
|
+
started_after = None
|
102
|
+
started_before = None
|
103
|
+
ended_after = None
|
104
|
+
ended_before = None
|
105
|
+
|
106
|
+
if started_spec:
|
107
|
+
started_after = parse_time_spec(started_spec)
|
108
|
+
if started_after is None:
|
109
|
+
click.echo(
|
110
|
+
f"Error: Could not parse start time '{started_spec}'", err=True
|
111
|
+
)
|
112
|
+
click.echo(
|
113
|
+
"Examples: 'today', 'last week', '2 hours ago', '2023-01-01'",
|
114
|
+
err=True,
|
115
|
+
)
|
116
|
+
raise click.Abort()
|
117
|
+
|
118
|
+
if ended_spec:
|
119
|
+
ended_after = parse_time_spec(ended_spec)
|
120
|
+
if ended_after is None:
|
121
|
+
click.echo(f"Error: Could not parse end time '{ended_spec}'", err=True)
|
122
|
+
click.echo(
|
123
|
+
"Examples: 'today', 'last week', '2 hours ago', '2023-01-01'",
|
124
|
+
err=True,
|
125
|
+
)
|
126
|
+
raise click.Abort()
|
127
|
+
|
128
|
+
if verbose:
|
129
|
+
click.echo("Filtering experiments...")
|
130
|
+
if status:
|
131
|
+
click.echo(f" Status: {status}")
|
132
|
+
if name_pattern:
|
133
|
+
click.echo(f" Name pattern: {name_pattern}")
|
134
|
+
if tags:
|
135
|
+
click.echo(f" Tags: {', '.join(tags)}")
|
136
|
+
if started_spec:
|
137
|
+
click.echo(f" Started: {started_spec}")
|
138
|
+
if ended_spec:
|
139
|
+
click.echo(f" Ended: {ended_spec}")
|
140
|
+
|
141
|
+
# Create filter and apply criteria
|
142
|
+
experiment_filter = ExperimentFilter()
|
143
|
+
|
144
|
+
# When showing archived experiments, we need to get all experiments first
|
145
|
+
# to avoid the default limit cutting off archived experiments
|
146
|
+
force_all = show_all or archived
|
147
|
+
|
148
|
+
experiments = experiment_filter.filter_experiments(
|
149
|
+
status=status,
|
150
|
+
name_pattern=name_pattern,
|
151
|
+
tags=list(tags) if tags else None,
|
152
|
+
started_after=started_after,
|
153
|
+
started_before=started_before,
|
154
|
+
ended_after=ended_after,
|
155
|
+
ended_before=ended_before,
|
156
|
+
limit=None if force_all else limit,
|
157
|
+
include_all=force_all,
|
158
|
+
include_archived=archived,
|
159
|
+
)
|
160
|
+
|
161
|
+
# Filter experiments based on archived flag
|
162
|
+
if archived:
|
163
|
+
experiments = [exp for exp in experiments if exp.get("archived", False)]
|
164
|
+
else:
|
165
|
+
experiments = [exp for exp in experiments if not exp.get("archived", False)]
|
166
|
+
|
167
|
+
# Apply limit after filtering by archived status if needed
|
168
|
+
if not show_all and limit is not None:
|
169
|
+
experiments = experiments[:limit]
|
170
|
+
elif not show_all and limit is None and not archived:
|
171
|
+
# Only apply default limit to regular experiments, not archived
|
172
|
+
experiments = experiments[:10]
|
173
|
+
|
174
|
+
if verbose:
|
175
|
+
click.echo(f"Found {len(experiments)} matching experiments")
|
176
|
+
|
177
|
+
# Format and display results
|
178
|
+
formatter = ExperimentTableFormatter()
|
179
|
+
|
180
|
+
if not experiments:
|
181
|
+
click.echo("No experiments found.")
|
182
|
+
_show_filter_suggestions(
|
183
|
+
status, name_pattern, tags, started_spec, ended_spec
|
184
|
+
)
|
185
|
+
return
|
186
|
+
|
187
|
+
# Display table
|
188
|
+
table_title = "Yanex Archived Experiments" if archived else "Yanex Experiments"
|
189
|
+
formatter.print_experiments_table(experiments, title=table_title)
|
190
|
+
|
191
|
+
# Show summary if filtering was applied or not showing all
|
192
|
+
if any([status, name_pattern, tags, started_spec, ended_spec]) or (
|
193
|
+
not show_all and limit != len(experiments)
|
194
|
+
):
|
195
|
+
# Get total count for summary
|
196
|
+
total_experiments = experiment_filter.filter_experiments(
|
197
|
+
include_all=True, include_archived=archived
|
198
|
+
)
|
199
|
+
|
200
|
+
# Filter total based on archived flag too
|
201
|
+
if archived:
|
202
|
+
total_experiments = [
|
203
|
+
exp for exp in total_experiments if exp.get("archived", False)
|
204
|
+
]
|
205
|
+
else:
|
206
|
+
total_experiments = [
|
207
|
+
exp for exp in total_experiments if not exp.get("archived", False)
|
208
|
+
]
|
209
|
+
|
210
|
+
formatter.print_summary(experiments, len(total_experiments))
|
211
|
+
|
212
|
+
except ValueError as e:
|
213
|
+
click.echo(f"Error: {e}", err=True)
|
214
|
+
raise click.Abort()
|
215
|
+
except Exception as e:
|
216
|
+
click.echo(f"Error listing experiments: {e}", err=True)
|
217
|
+
if verbose:
|
218
|
+
import traceback
|
219
|
+
|
220
|
+
click.echo(traceback.format_exc(), err=True)
|
221
|
+
raise click.Abort()
|
222
|
+
|
223
|
+
|
224
|
+
def _show_filter_suggestions(
|
225
|
+
status: Optional[str],
|
226
|
+
name_pattern: Optional[str],
|
227
|
+
tags: List[str],
|
228
|
+
started_spec: Optional[str],
|
229
|
+
ended_spec: Optional[str],
|
230
|
+
) -> None:
|
231
|
+
"""Show helpful suggestions when no experiments are found."""
|
232
|
+
|
233
|
+
# Check if any filters were applied
|
234
|
+
has_filters = any([status, name_pattern, tags, started_spec, ended_spec])
|
235
|
+
|
236
|
+
if has_filters:
|
237
|
+
click.echo("\nTry adjusting your filters:")
|
238
|
+
click.echo(" • Remove some filters to see more results")
|
239
|
+
click.echo(" • Use 'yanex list --all' to see all experiments")
|
240
|
+
click.echo(" • Check status with: yanex list --status completed")
|
241
|
+
else:
|
242
|
+
click.echo("\nNo experiments found. To create your first experiment:")
|
243
|
+
click.echo(" yanex run your_script.py")
|