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
|
+
Unarchive experiments - move them back from 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("unarchive")
|
19
|
+
@click.argument("experiment_identifiers", nargs=-1)
|
20
|
+
@click.option(
|
21
|
+
"--status",
|
22
|
+
type=click.Choice(EXPERIMENT_STATUSES),
|
23
|
+
help="Unarchive experiments with specific status",
|
24
|
+
)
|
25
|
+
@click.option(
|
26
|
+
"--name",
|
27
|
+
"name_pattern",
|
28
|
+
help="Unarchive experiments matching name pattern (glob syntax)",
|
29
|
+
)
|
30
|
+
@click.option(
|
31
|
+
"--tag", "tags", multiple=True, help="Unarchive experiments with ALL specified tags"
|
32
|
+
)
|
33
|
+
@click.option(
|
34
|
+
"--started-after",
|
35
|
+
help="Unarchive experiments started after date/time (e.g., '2025-01-01', 'yesterday', '1 week ago')",
|
36
|
+
)
|
37
|
+
@click.option("--started-before", help="Unarchive experiments started before date/time")
|
38
|
+
@click.option("--ended-after", help="Unarchive experiments ended after date/time")
|
39
|
+
@click.option("--ended-before", help="Unarchive experiments ended before date/time")
|
40
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
41
|
+
@click.pass_context
|
42
|
+
def unarchive_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
|
+
Unarchive experiments by moving them back to experiments 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 unarchive exp1 exp2 # Unarchive specific experiments
|
63
|
+
yanex unarchive --status completed # Unarchive all completed experiments
|
64
|
+
yanex unarchive --name "*training*" # Unarchive experiments with "training" in name
|
65
|
+
yanex unarchive --tag experiment-v1 # Unarchive experiments with specific tag
|
66
|
+
"""
|
67
|
+
try:
|
68
|
+
filter_obj = ExperimentFilter()
|
69
|
+
|
70
|
+
# Validate mutually exclusive targeting
|
71
|
+
has_identifiers = len(experiment_identifiers) > 0
|
72
|
+
has_filters = any(
|
73
|
+
[
|
74
|
+
status,
|
75
|
+
name_pattern,
|
76
|
+
tags,
|
77
|
+
started_after,
|
78
|
+
started_before,
|
79
|
+
ended_after,
|
80
|
+
ended_before,
|
81
|
+
]
|
82
|
+
)
|
83
|
+
|
84
|
+
if has_identifiers and has_filters:
|
85
|
+
click.echo(
|
86
|
+
"Error: Cannot use both experiment identifiers and filter options. Choose one approach.",
|
87
|
+
err=True,
|
88
|
+
)
|
89
|
+
ctx.exit(1)
|
90
|
+
|
91
|
+
if not has_identifiers and not has_filters:
|
92
|
+
click.echo(
|
93
|
+
"Error: Must specify either experiment identifiers or filter options",
|
94
|
+
err=True,
|
95
|
+
)
|
96
|
+
ctx.exit(1)
|
97
|
+
|
98
|
+
# Parse time specifications
|
99
|
+
started_after_dt = parse_time_spec(started_after) if started_after else None
|
100
|
+
started_before_dt = parse_time_spec(started_before) if started_before else None
|
101
|
+
ended_after_dt = parse_time_spec(ended_after) if ended_after else None
|
102
|
+
ended_before_dt = parse_time_spec(ended_before) if ended_before else None
|
103
|
+
|
104
|
+
# Find experiments to unarchive
|
105
|
+
if experiment_identifiers:
|
106
|
+
# Unarchive specific experiments by ID/name
|
107
|
+
experiments = find_experiments_by_identifiers(
|
108
|
+
filter_obj,
|
109
|
+
list(experiment_identifiers),
|
110
|
+
archived_only=True, # Only search archived experiments
|
111
|
+
)
|
112
|
+
else:
|
113
|
+
# Unarchive experiments by filter criteria
|
114
|
+
|
115
|
+
experiments = find_experiments_by_filters(
|
116
|
+
filter_obj,
|
117
|
+
status=status,
|
118
|
+
name_pattern=name_pattern,
|
119
|
+
tags=list(tags) if tags else None,
|
120
|
+
started_after=started_after_dt,
|
121
|
+
started_before=started_before_dt,
|
122
|
+
ended_after=ended_after_dt,
|
123
|
+
ended_before=ended_before_dt,
|
124
|
+
include_archived=True, # Only search archived experiments
|
125
|
+
)
|
126
|
+
|
127
|
+
# Filter to only archived experiments
|
128
|
+
experiments = [exp for exp in experiments if exp.get("archived", False)]
|
129
|
+
|
130
|
+
if not experiments:
|
131
|
+
click.echo("No archived experiments found to unarchive.")
|
132
|
+
return
|
133
|
+
|
134
|
+
# Show experiments and get confirmation
|
135
|
+
if not confirm_experiment_operation(
|
136
|
+
experiments, "unarchive", force, "unarchived", default_yes=True
|
137
|
+
):
|
138
|
+
click.echo("Unarchive operation cancelled.")
|
139
|
+
return
|
140
|
+
|
141
|
+
# Unarchive experiments
|
142
|
+
click.echo(f"Unarchiving {len(experiments)} experiment(s)...")
|
143
|
+
|
144
|
+
success_count = 0
|
145
|
+
for exp in experiments:
|
146
|
+
try:
|
147
|
+
experiment_id = exp["id"]
|
148
|
+
filter_obj.manager.storage.unarchive_experiment(experiment_id)
|
149
|
+
|
150
|
+
# Show progress
|
151
|
+
exp_name = exp.get("name", "[unnamed]")
|
152
|
+
click.echo(f" ✓ Unarchived {experiment_id} ({exp_name})")
|
153
|
+
success_count += 1
|
154
|
+
|
155
|
+
except Exception as e:
|
156
|
+
exp_name = exp.get("name", "[unnamed]")
|
157
|
+
click.echo(
|
158
|
+
f" ✗ Failed to unarchive {experiment_id} ({exp_name}): {e}",
|
159
|
+
err=True,
|
160
|
+
)
|
161
|
+
|
162
|
+
# Summary
|
163
|
+
if success_count == len(experiments):
|
164
|
+
click.echo(f"Successfully unarchived {success_count} experiment(s).")
|
165
|
+
else:
|
166
|
+
failed_count = len(experiments) - success_count
|
167
|
+
click.echo(
|
168
|
+
f"Unarchived {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,282 @@
|
|
1
|
+
"""
|
2
|
+
Update experiment metadata - name, description, status, and tags.
|
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("update")
|
19
|
+
@click.argument("experiment_identifiers", nargs=-1)
|
20
|
+
@click.option(
|
21
|
+
"--status",
|
22
|
+
"filter_status",
|
23
|
+
type=click.Choice(EXPERIMENT_STATUSES),
|
24
|
+
help="Filter experiments by status for bulk updates",
|
25
|
+
)
|
26
|
+
@click.option(
|
27
|
+
"--name",
|
28
|
+
"filter_name_pattern",
|
29
|
+
help="Filter experiments by name pattern for bulk updates (glob syntax)",
|
30
|
+
)
|
31
|
+
@click.option(
|
32
|
+
"--tag",
|
33
|
+
"filter_tags",
|
34
|
+
multiple=True,
|
35
|
+
help="Filter experiments by tag for bulk updates (experiments must have ALL specified tags)",
|
36
|
+
)
|
37
|
+
@click.option(
|
38
|
+
"--started-after",
|
39
|
+
help="Filter experiments started after date/time for bulk updates",
|
40
|
+
)
|
41
|
+
@click.option(
|
42
|
+
"--started-before",
|
43
|
+
help="Filter experiments started before date/time for bulk updates",
|
44
|
+
)
|
45
|
+
@click.option(
|
46
|
+
"--ended-after", help="Filter experiments ended after date/time for bulk updates"
|
47
|
+
)
|
48
|
+
@click.option(
|
49
|
+
"--ended-before", help="Filter experiments ended before date/time for bulk updates"
|
50
|
+
)
|
51
|
+
@click.option(
|
52
|
+
"--set-name", "new_name", help="Set experiment name (use empty string to clear)"
|
53
|
+
)
|
54
|
+
@click.option(
|
55
|
+
"--set-description",
|
56
|
+
"new_description",
|
57
|
+
help="Set experiment description (use empty string to clear)",
|
58
|
+
)
|
59
|
+
@click.option(
|
60
|
+
"--set-status",
|
61
|
+
"new_status",
|
62
|
+
type=click.Choice(EXPERIMENT_STATUSES),
|
63
|
+
help="Set experiment status",
|
64
|
+
)
|
65
|
+
@click.option(
|
66
|
+
"--add-tag", "add_tags", multiple=True, help="Add tag to experiment(s) (repeatable)"
|
67
|
+
)
|
68
|
+
@click.option(
|
69
|
+
"--remove-tag",
|
70
|
+
"remove_tags",
|
71
|
+
multiple=True,
|
72
|
+
help="Remove tag from experiment(s) (repeatable)",
|
73
|
+
)
|
74
|
+
@click.option("--archived", is_flag=True, help="Update archived experiments")
|
75
|
+
@click.option(
|
76
|
+
"--force", is_flag=True, help="Skip confirmation prompt for bulk operations"
|
77
|
+
)
|
78
|
+
@click.option(
|
79
|
+
"--dry-run", is_flag=True, help="Show what would be updated without making changes"
|
80
|
+
)
|
81
|
+
@click.pass_context
|
82
|
+
def update_experiments(
|
83
|
+
ctx,
|
84
|
+
experiment_identifiers: tuple,
|
85
|
+
filter_status: Optional[str],
|
86
|
+
filter_name_pattern: Optional[str],
|
87
|
+
filter_tags: tuple,
|
88
|
+
started_after: Optional[str],
|
89
|
+
started_before: Optional[str],
|
90
|
+
ended_after: Optional[str],
|
91
|
+
ended_before: Optional[str],
|
92
|
+
new_name: Optional[str],
|
93
|
+
new_description: Optional[str],
|
94
|
+
new_status: Optional[str],
|
95
|
+
add_tags: tuple,
|
96
|
+
remove_tags: tuple,
|
97
|
+
archived: bool,
|
98
|
+
force: bool,
|
99
|
+
dry_run: bool,
|
100
|
+
):
|
101
|
+
"""
|
102
|
+
Update experiment metadata including name, description, status, and tags.
|
103
|
+
|
104
|
+
EXPERIMENT_IDENTIFIERS can be experiment IDs or names.
|
105
|
+
For bulk updates, use filter options instead of identifiers.
|
106
|
+
|
107
|
+
Examples:
|
108
|
+
\\b
|
109
|
+
# Update single experiment
|
110
|
+
yanex update exp1 --set-name "New Name" --set-description "New description"
|
111
|
+
yanex update "my experiment" --add-tag production --remove-tag testing
|
112
|
+
yanex update exp123 --set-status completed
|
113
|
+
|
114
|
+
# Bulk updates with filters
|
115
|
+
yanex update --status failed --set-description "Failed batch run"
|
116
|
+
yanex update --tag experimental --remove-tag experimental --add-tag archived
|
117
|
+
yanex update --ended-before "1 week ago" --add-tag old-runs
|
118
|
+
|
119
|
+
# Preview changes without applying
|
120
|
+
yanex update exp1 --set-name "New Name" --dry-run
|
121
|
+
"""
|
122
|
+
try:
|
123
|
+
filter_obj = ExperimentFilter()
|
124
|
+
|
125
|
+
# Validate that we have something to update
|
126
|
+
if not any(
|
127
|
+
[
|
128
|
+
new_name is not None,
|
129
|
+
new_description is not None,
|
130
|
+
new_status,
|
131
|
+
add_tags,
|
132
|
+
remove_tags,
|
133
|
+
]
|
134
|
+
):
|
135
|
+
click.echo(
|
136
|
+
"Error: Must specify at least one update option (--set-name, --set-description, --set-status, --add-tag, --remove-tag)",
|
137
|
+
err=True,
|
138
|
+
)
|
139
|
+
ctx.exit(1)
|
140
|
+
|
141
|
+
# Validate mutually exclusive targeting
|
142
|
+
has_identifiers = len(experiment_identifiers) > 0
|
143
|
+
has_filters = any(
|
144
|
+
[
|
145
|
+
filter_status,
|
146
|
+
filter_name_pattern,
|
147
|
+
filter_tags,
|
148
|
+
started_after,
|
149
|
+
started_before,
|
150
|
+
ended_after,
|
151
|
+
ended_before,
|
152
|
+
]
|
153
|
+
)
|
154
|
+
|
155
|
+
if has_identifiers and has_filters:
|
156
|
+
click.echo(
|
157
|
+
"Error: Cannot use both experiment identifiers and filter options. Choose one approach.",
|
158
|
+
err=True,
|
159
|
+
)
|
160
|
+
ctx.exit(1)
|
161
|
+
|
162
|
+
if not has_identifiers and not has_filters:
|
163
|
+
click.echo(
|
164
|
+
"Error: Must specify either experiment identifiers or filter options",
|
165
|
+
err=True,
|
166
|
+
)
|
167
|
+
ctx.exit(1)
|
168
|
+
|
169
|
+
# Parse time specifications
|
170
|
+
started_after_dt = parse_time_spec(started_after) if started_after else None
|
171
|
+
started_before_dt = parse_time_spec(started_before) if started_before else None
|
172
|
+
ended_after_dt = parse_time_spec(ended_after) if ended_after else None
|
173
|
+
ended_before_dt = parse_time_spec(ended_before) if ended_before else None
|
174
|
+
|
175
|
+
# Find experiments to update
|
176
|
+
if experiment_identifiers:
|
177
|
+
# Update specific experiments by ID/name
|
178
|
+
experiments = find_experiments_by_identifiers(
|
179
|
+
filter_obj, list(experiment_identifiers), include_archived=archived
|
180
|
+
)
|
181
|
+
else:
|
182
|
+
# Update experiments by filter criteria
|
183
|
+
experiments = find_experiments_by_filters(
|
184
|
+
filter_obj,
|
185
|
+
status=filter_status,
|
186
|
+
name_pattern=filter_name_pattern,
|
187
|
+
tags=list(filter_tags) if filter_tags else None,
|
188
|
+
started_after=started_after_dt,
|
189
|
+
started_before=started_before_dt,
|
190
|
+
ended_after=ended_after_dt,
|
191
|
+
ended_before=ended_before_dt,
|
192
|
+
include_archived=archived,
|
193
|
+
)
|
194
|
+
|
195
|
+
# Filter based on archived flag
|
196
|
+
if archived:
|
197
|
+
experiments = [exp for exp in experiments if exp.get("archived", False)]
|
198
|
+
else:
|
199
|
+
experiments = [exp for exp in experiments if not exp.get("archived", False)]
|
200
|
+
|
201
|
+
if not experiments:
|
202
|
+
location = "archived" if archived else "regular"
|
203
|
+
click.echo(f"No {location} experiments found to update.")
|
204
|
+
return
|
205
|
+
|
206
|
+
# Prepare update dictionary
|
207
|
+
updates = {}
|
208
|
+
|
209
|
+
if new_name is not None:
|
210
|
+
updates["name"] = new_name
|
211
|
+
if new_description is not None:
|
212
|
+
updates["description"] = new_description
|
213
|
+
if new_status:
|
214
|
+
updates["status"] = new_status
|
215
|
+
if add_tags:
|
216
|
+
updates["add_tags"] = list(add_tags)
|
217
|
+
if remove_tags:
|
218
|
+
updates["remove_tags"] = list(remove_tags)
|
219
|
+
|
220
|
+
# Show what will be updated
|
221
|
+
click.echo("Updates to apply:")
|
222
|
+
for key, value in updates.items():
|
223
|
+
if key == "add_tags":
|
224
|
+
click.echo(f" Add tags: {', '.join(value)}")
|
225
|
+
elif key == "remove_tags":
|
226
|
+
click.echo(f" Remove tags: {', '.join(value)}")
|
227
|
+
elif key in ["name", "description"] and value == "":
|
228
|
+
click.echo(f" Clear {key}")
|
229
|
+
else:
|
230
|
+
click.echo(f" Set {key}: {value}")
|
231
|
+
click.echo()
|
232
|
+
|
233
|
+
# Show experiments and get confirmation for bulk operations or dry run
|
234
|
+
if len(experiments) > 1 or dry_run:
|
235
|
+
if not confirm_experiment_operation(
|
236
|
+
experiments, "update", force or dry_run, "updated"
|
237
|
+
):
|
238
|
+
click.echo("Update operation cancelled.")
|
239
|
+
return
|
240
|
+
|
241
|
+
if dry_run:
|
242
|
+
click.echo("Dry run completed. No changes were made.")
|
243
|
+
return
|
244
|
+
|
245
|
+
# Update experiments
|
246
|
+
click.echo(f"Updating {len(experiments)} experiment(s)...")
|
247
|
+
|
248
|
+
success_count = 0
|
249
|
+
for exp in experiments:
|
250
|
+
try:
|
251
|
+
experiment_id = exp["id"]
|
252
|
+
filter_obj.manager.storage.update_experiment_metadata(
|
253
|
+
experiment_id, updates, include_archived=archived
|
254
|
+
)
|
255
|
+
|
256
|
+
# Show progress
|
257
|
+
exp_name = exp.get("name", "[unnamed]")
|
258
|
+
click.echo(f" ✓ Updated {experiment_id} ({exp_name})")
|
259
|
+
success_count += 1
|
260
|
+
|
261
|
+
except Exception as e:
|
262
|
+
exp_name = exp.get("name", "[unnamed]")
|
263
|
+
click.echo(
|
264
|
+
f" ✗ Failed to update {experiment_id} ({exp_name}): {e}", err=True
|
265
|
+
)
|
266
|
+
|
267
|
+
# Summary
|
268
|
+
if success_count == len(experiments):
|
269
|
+
click.echo(f"Successfully updated {success_count} experiment(s).")
|
270
|
+
else:
|
271
|
+
failed_count = len(experiments) - success_count
|
272
|
+
click.echo(
|
273
|
+
f"Updated {success_count} experiment(s), {failed_count} failed.",
|
274
|
+
err=True,
|
275
|
+
)
|
276
|
+
ctx.exit(1)
|
277
|
+
|
278
|
+
except click.ClickException:
|
279
|
+
raise # Re-raise ClickException to show proper error message
|
280
|
+
except Exception as e:
|
281
|
+
click.echo(f"Error: {e}", err=True)
|
282
|
+
ctx.exit(1)
|