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.
@@ -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)
@@ -0,0 +1,8 @@
1
+ """
2
+ Filtering components for yanex CLI commands.
3
+ """
4
+
5
+ from .base import ExperimentFilter
6
+ from .time_utils import parse_time_spec
7
+
8
+ __all__ = ["ExperimentFilter", "parse_time_spec"]