dayhoff-tools 1.1.10__py3-none-any.whl → 1.13.12__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.
- dayhoff_tools/__init__.py +10 -0
- dayhoff_tools/cli/cloud_commands.py +179 -43
- dayhoff_tools/cli/engine1/__init__.py +323 -0
- dayhoff_tools/cli/engine1/engine_core.py +703 -0
- dayhoff_tools/cli/engine1/engine_lifecycle.py +136 -0
- dayhoff_tools/cli/engine1/engine_maintenance.py +431 -0
- dayhoff_tools/cli/engine1/engine_management.py +505 -0
- dayhoff_tools/cli/engine1/shared.py +501 -0
- dayhoff_tools/cli/engine1/studio_commands.py +825 -0
- dayhoff_tools/cli/engines_studios/__init__.py +6 -0
- dayhoff_tools/cli/engines_studios/api_client.py +351 -0
- dayhoff_tools/cli/engines_studios/auth.py +144 -0
- dayhoff_tools/cli/engines_studios/engine-studio-cli.md +1230 -0
- dayhoff_tools/cli/engines_studios/engine_commands.py +1151 -0
- dayhoff_tools/cli/engines_studios/progress.py +260 -0
- dayhoff_tools/cli/engines_studios/simulators/cli-simulators.md +151 -0
- dayhoff_tools/cli/engines_studios/simulators/demo.sh +75 -0
- dayhoff_tools/cli/engines_studios/simulators/engine_list_simulator.py +319 -0
- dayhoff_tools/cli/engines_studios/simulators/engine_status_simulator.py +369 -0
- dayhoff_tools/cli/engines_studios/simulators/idle_status_simulator.py +476 -0
- dayhoff_tools/cli/engines_studios/simulators/simulator_utils.py +180 -0
- dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py +374 -0
- dayhoff_tools/cli/engines_studios/simulators/studio_status_simulator.py +164 -0
- dayhoff_tools/cli/engines_studios/studio_commands.py +755 -0
- dayhoff_tools/cli/main.py +106 -7
- dayhoff_tools/cli/utility_commands.py +896 -179
- dayhoff_tools/deployment/base.py +70 -6
- dayhoff_tools/deployment/deploy_aws.py +165 -25
- dayhoff_tools/deployment/deploy_gcp.py +78 -5
- dayhoff_tools/deployment/deploy_utils.py +20 -7
- dayhoff_tools/deployment/job_runner.py +9 -4
- dayhoff_tools/deployment/processors.py +230 -418
- dayhoff_tools/deployment/swarm.py +47 -12
- dayhoff_tools/embedders.py +28 -26
- dayhoff_tools/fasta.py +181 -64
- dayhoff_tools/warehouse.py +268 -1
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/METADATA +20 -5
- dayhoff_tools-1.13.12.dist-info/RECORD +54 -0
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/WHEEL +1 -1
- dayhoff_tools-1.1.10.dist-info/RECORD +0 -32
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"""Studio CLI commands for engines_studios system."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from .api_client import StudioManagerClient
|
|
9
|
+
from .auth import check_aws_auth, detect_aws_environment, get_aws_username
|
|
10
|
+
from .progress import format_time_ago, wait_with_progress
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def studio_cli():
|
|
15
|
+
"""Manage studios."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# Lifecycle Management
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@studio_cli.command("create")
|
|
25
|
+
@click.option("--size", "size_gb", type=int, default=100, help="Studio size in GB")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--user",
|
|
28
|
+
default=None,
|
|
29
|
+
help="User to create studio for (defaults to current user, use for testing/admin)",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--env",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
35
|
+
)
|
|
36
|
+
def create_studio(size_gb: int, user: Optional[str], env: Optional[str]):
|
|
37
|
+
"""Create a new studio for the current user (or specified user with --user flag)."""
|
|
38
|
+
|
|
39
|
+
# Check AWS auth first to provide clear error messages
|
|
40
|
+
check_aws_auth()
|
|
41
|
+
|
|
42
|
+
# Auto-detect environment if not specified
|
|
43
|
+
if env is None:
|
|
44
|
+
env = detect_aws_environment()
|
|
45
|
+
click.echo(f"🔍 Detected environment: {env}")
|
|
46
|
+
|
|
47
|
+
# Require confirmation for non-dev environments
|
|
48
|
+
if env != "dev":
|
|
49
|
+
if not click.confirm(
|
|
50
|
+
f"⚠️ You are about to create in {env.upper()}. Continue?"
|
|
51
|
+
):
|
|
52
|
+
click.echo("Cancelled")
|
|
53
|
+
raise click.Abort()
|
|
54
|
+
|
|
55
|
+
client = StudioManagerClient(environment=env)
|
|
56
|
+
|
|
57
|
+
# Get user (from flag or current AWS user)
|
|
58
|
+
if user is None:
|
|
59
|
+
try:
|
|
60
|
+
user = get_aws_username()
|
|
61
|
+
except RuntimeError as e:
|
|
62
|
+
click.echo(f"✗ {e}", err=True)
|
|
63
|
+
raise click.Abort()
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Check if user already has a studio (only for current user)
|
|
67
|
+
try:
|
|
68
|
+
current_aws_user = get_aws_username()
|
|
69
|
+
if user == current_aws_user:
|
|
70
|
+
existing = client.get_my_studio()
|
|
71
|
+
if existing:
|
|
72
|
+
click.echo(
|
|
73
|
+
f"✗ You already have a studio: {existing['studio_id']}",
|
|
74
|
+
err=True,
|
|
75
|
+
)
|
|
76
|
+
click.echo(f" Use 'dh studio delete' to remove it first")
|
|
77
|
+
raise click.Abort()
|
|
78
|
+
except click.Abort:
|
|
79
|
+
# Re-raise Abort so it propagates correctly
|
|
80
|
+
raise
|
|
81
|
+
except Exception:
|
|
82
|
+
# If we can't get current user for other reasons, skip the check
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
click.echo(f"Creating {size_gb}GB studio for {user}...")
|
|
86
|
+
|
|
87
|
+
studio = client.create_studio(user=user, size_gb=size_gb)
|
|
88
|
+
|
|
89
|
+
if "error" in studio:
|
|
90
|
+
click.echo(f"✗ Error: {studio['error']}", err=True)
|
|
91
|
+
raise click.Abort()
|
|
92
|
+
|
|
93
|
+
studio_id = studio["studio_id"]
|
|
94
|
+
click.echo(f"✓ Studio created: {studio_id}")
|
|
95
|
+
click.echo(f"\nAttach to an engine with:")
|
|
96
|
+
click.echo(f" dh studio attach <engine-name>")
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
100
|
+
raise click.Abort()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@studio_cli.command("delete")
|
|
104
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
105
|
+
@click.option(
|
|
106
|
+
"--user",
|
|
107
|
+
default=None,
|
|
108
|
+
help="User whose studio to delete (defaults to current user, use for testing/admin)",
|
|
109
|
+
)
|
|
110
|
+
@click.option(
|
|
111
|
+
"--env",
|
|
112
|
+
default=None,
|
|
113
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
114
|
+
)
|
|
115
|
+
def delete_studio(yes: bool, user: Optional[str], env: Optional[str]):
|
|
116
|
+
"""Delete your studio (or another user's studio with --user flag)."""
|
|
117
|
+
|
|
118
|
+
# Check AWS auth first to provide clear error messages
|
|
119
|
+
check_aws_auth()
|
|
120
|
+
|
|
121
|
+
# Auto-detect environment if not specified
|
|
122
|
+
if env is None:
|
|
123
|
+
env = detect_aws_environment()
|
|
124
|
+
|
|
125
|
+
client = StudioManagerClient(environment=env)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Get studio (for current user or specified user)
|
|
129
|
+
if user is None:
|
|
130
|
+
studio = client.get_my_studio()
|
|
131
|
+
if not studio:
|
|
132
|
+
click.echo("You don't have a studio")
|
|
133
|
+
return
|
|
134
|
+
else:
|
|
135
|
+
# Get studio by user - list all and filter
|
|
136
|
+
result = client.list_studios()
|
|
137
|
+
studios = result.get("studios", [])
|
|
138
|
+
user_studios = [s for s in studios if s.get("user") == user]
|
|
139
|
+
if not user_studios:
|
|
140
|
+
click.echo(f"User '{user}' doesn't have a studio")
|
|
141
|
+
return
|
|
142
|
+
studio = user_studios[0]
|
|
143
|
+
|
|
144
|
+
studio_id = studio["studio_id"]
|
|
145
|
+
|
|
146
|
+
# Must be detached first
|
|
147
|
+
if studio["status"] == "attached":
|
|
148
|
+
click.echo("✗ Studio must be detached before deletion", err=True)
|
|
149
|
+
click.echo(" Run: dh studio detach")
|
|
150
|
+
raise click.Abort()
|
|
151
|
+
|
|
152
|
+
# Confirm
|
|
153
|
+
if not yes:
|
|
154
|
+
click.echo(
|
|
155
|
+
f"⚠ WARNING: This will permanently delete all data in {studio_id}"
|
|
156
|
+
)
|
|
157
|
+
if not click.confirm("Are you sure?"):
|
|
158
|
+
click.echo("Cancelled")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Delete
|
|
162
|
+
result = client.delete_studio(studio_id)
|
|
163
|
+
|
|
164
|
+
if "error" in result:
|
|
165
|
+
click.echo(f"✗ Error: {result['error']}", err=True)
|
|
166
|
+
raise click.Abort()
|
|
167
|
+
|
|
168
|
+
click.echo(f"✓ Studio {studio_id} deleted")
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
172
|
+
raise click.Abort()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ============================================================================
|
|
176
|
+
# Status and Information
|
|
177
|
+
# ============================================================================
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@studio_cli.command("status")
|
|
181
|
+
@click.option(
|
|
182
|
+
"--user",
|
|
183
|
+
default=None,
|
|
184
|
+
help="User whose studio status to check (defaults to current user, use for testing/admin)",
|
|
185
|
+
)
|
|
186
|
+
@click.option(
|
|
187
|
+
"--env",
|
|
188
|
+
default=None,
|
|
189
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
190
|
+
)
|
|
191
|
+
def studio_status(user: Optional[str], env: Optional[str]):
|
|
192
|
+
"""Show information about your studio."""
|
|
193
|
+
|
|
194
|
+
# Check AWS auth first to provide clear error messages
|
|
195
|
+
check_aws_auth()
|
|
196
|
+
|
|
197
|
+
# Auto-detect environment if not specified
|
|
198
|
+
if env is None:
|
|
199
|
+
env = detect_aws_environment()
|
|
200
|
+
|
|
201
|
+
client = StudioManagerClient(environment=env)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# Get studio (for current user or specified user)
|
|
205
|
+
if user is None:
|
|
206
|
+
studio = client.get_my_studio()
|
|
207
|
+
if not studio:
|
|
208
|
+
click.echo("You don't have a studio yet. Create one with:")
|
|
209
|
+
click.echo(" dh studio create")
|
|
210
|
+
return
|
|
211
|
+
else:
|
|
212
|
+
# Get studio by user - list all and filter
|
|
213
|
+
result = client.list_studios()
|
|
214
|
+
studios = result.get("studios", [])
|
|
215
|
+
user_studios = [s for s in studios if s.get("user") == user]
|
|
216
|
+
if not user_studios:
|
|
217
|
+
click.echo(f"User '{user}' doesn't have a studio")
|
|
218
|
+
return
|
|
219
|
+
studio = user_studios[0]
|
|
220
|
+
|
|
221
|
+
# Reordered output: User, Status, Attached to, Account, Size, Created, Studio ID
|
|
222
|
+
click.echo(f"User: {studio['user']}")
|
|
223
|
+
# Status in blue
|
|
224
|
+
click.echo(f"Status: \033[34m{studio['status']}\033[0m")
|
|
225
|
+
# Attached to in blue (if present) - resolve instance ID to engine name
|
|
226
|
+
if studio.get("attached_to"):
|
|
227
|
+
instance_id = studio["attached_to"]
|
|
228
|
+
# Try to resolve instance ID to engine name by searching engines list
|
|
229
|
+
engine_name = instance_id # Default to instance ID if not found
|
|
230
|
+
try:
|
|
231
|
+
engines_result = client.list_engines()
|
|
232
|
+
for engine in engines_result.get("engines", []):
|
|
233
|
+
if engine.get("instance_id") == instance_id:
|
|
234
|
+
engine_name = engine.get("name", instance_id)
|
|
235
|
+
break
|
|
236
|
+
except Exception:
|
|
237
|
+
pass # Fall back to instance ID
|
|
238
|
+
click.echo(f"Attached to: \033[34m{engine_name}\033[0m")
|
|
239
|
+
click.echo(f"Account: {env}")
|
|
240
|
+
click.echo(f"Size: {studio['size_gb']}GB")
|
|
241
|
+
if studio.get("created_at"):
|
|
242
|
+
click.echo(f"Created: {format_time_ago(studio['created_at'])}")
|
|
243
|
+
click.echo(f"Studio ID: {studio['studio_id']}")
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
247
|
+
raise click.Abort()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@studio_cli.command("list")
|
|
251
|
+
@click.option(
|
|
252
|
+
"--env",
|
|
253
|
+
default=None,
|
|
254
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
255
|
+
)
|
|
256
|
+
def list_studios(env: Optional[str]):
|
|
257
|
+
"""List all studios."""
|
|
258
|
+
|
|
259
|
+
# Check AWS auth first to provide clear error messages
|
|
260
|
+
check_aws_auth()
|
|
261
|
+
|
|
262
|
+
# Auto-detect environment if not specified
|
|
263
|
+
if env is None:
|
|
264
|
+
env = detect_aws_environment()
|
|
265
|
+
|
|
266
|
+
client = StudioManagerClient(environment=env)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
result = client.list_studios()
|
|
270
|
+
studios = result.get("studios", [])
|
|
271
|
+
|
|
272
|
+
# Show account header with blue account name
|
|
273
|
+
click.echo(f"\nStudios for AWS Account \033[34m{env}\033[0m")
|
|
274
|
+
|
|
275
|
+
if not studios:
|
|
276
|
+
click.echo("No studios found\n")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Get all engines to map instance IDs to names
|
|
280
|
+
engines_result = client.list_engines()
|
|
281
|
+
engines_map = {}
|
|
282
|
+
for engine in engines_result.get("engines", []):
|
|
283
|
+
engines_map[engine["instance_id"]] = engine["name"]
|
|
284
|
+
|
|
285
|
+
# Calculate dynamic width for User column (longest user + 2 for padding)
|
|
286
|
+
max_user_len = max(
|
|
287
|
+
(len(studio.get("user", "unknown")) for studio in studios), default=4
|
|
288
|
+
)
|
|
289
|
+
user_width = max(max_user_len + 2, len("User") + 2)
|
|
290
|
+
|
|
291
|
+
# Calculate dynamic width for Attached To column
|
|
292
|
+
max_attached_len = 0
|
|
293
|
+
for studio in studios:
|
|
294
|
+
if studio.get("attached_to"):
|
|
295
|
+
instance_id = studio["attached_to"]
|
|
296
|
+
engine_name = engines_map.get(instance_id, "unknown")
|
|
297
|
+
max_attached_len = max(max_attached_len, len(engine_name))
|
|
298
|
+
attached_width = max(
|
|
299
|
+
max_attached_len + 2, len("Attached To") + 2, 3
|
|
300
|
+
) # At least 3 for "-"
|
|
301
|
+
|
|
302
|
+
# Fixed widths for other columns - reordered to [User, Status, Attached To, Size, Studio ID]
|
|
303
|
+
status_width = 12
|
|
304
|
+
size_width = 10
|
|
305
|
+
id_width = 25
|
|
306
|
+
|
|
307
|
+
# Table top border
|
|
308
|
+
click.echo(
|
|
309
|
+
"╭"
|
|
310
|
+
+ "─" * (user_width + 1)
|
|
311
|
+
+ "┬"
|
|
312
|
+
+ "─" * (status_width + 1)
|
|
313
|
+
+ "┬"
|
|
314
|
+
+ "─" * (attached_width + 1)
|
|
315
|
+
+ "┬"
|
|
316
|
+
+ "─" * (size_width + 1)
|
|
317
|
+
+ "┬"
|
|
318
|
+
+ "─" * (id_width + 1)
|
|
319
|
+
+ "╮"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Table header - reordered to [User, Status, Attached To, Size, Studio ID]
|
|
323
|
+
click.echo(
|
|
324
|
+
f"│ {'User':<{user_width}}│ {'Status':<{status_width}}│ {'Attached To':<{attached_width}}│ {'Size':<{size_width}}│ {'Studio ID':<{id_width}}│"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Header separator
|
|
328
|
+
click.echo(
|
|
329
|
+
"├"
|
|
330
|
+
+ "─" * (user_width + 1)
|
|
331
|
+
+ "┼"
|
|
332
|
+
+ "─" * (status_width + 1)
|
|
333
|
+
+ "┼"
|
|
334
|
+
+ "─" * (attached_width + 1)
|
|
335
|
+
+ "┼"
|
|
336
|
+
+ "─" * (size_width + 1)
|
|
337
|
+
+ "┼"
|
|
338
|
+
+ "─" * (id_width + 1)
|
|
339
|
+
+ "┤"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Table rows
|
|
343
|
+
for studio in studios:
|
|
344
|
+
user = studio.get("user", "unknown")
|
|
345
|
+
status = studio.get("status", "unknown")
|
|
346
|
+
size = f"{studio.get('size_gb', 0)}GB"
|
|
347
|
+
studio_id = studio.get("studio_id", "unknown")
|
|
348
|
+
attached_to = studio.get("attached_to")
|
|
349
|
+
|
|
350
|
+
# Truncate if needed
|
|
351
|
+
if len(user) > user_width - 1:
|
|
352
|
+
user = user[: user_width - 1]
|
|
353
|
+
|
|
354
|
+
# Color the user (blue)
|
|
355
|
+
user_display = f"\033[34m{user:<{user_width}}\033[0m"
|
|
356
|
+
|
|
357
|
+
# Format status - display "in-use" as "attached" in purple
|
|
358
|
+
if status == "in-use":
|
|
359
|
+
display_status = "attached"
|
|
360
|
+
status_display = (
|
|
361
|
+
f"\033[35m{display_status:<{status_width}}\033[0m" # Purple
|
|
362
|
+
)
|
|
363
|
+
elif status == "available":
|
|
364
|
+
status_display = f"\033[32m{status:<{status_width}}\033[0m" # Green
|
|
365
|
+
elif status in ["attaching", "detaching"]:
|
|
366
|
+
status_display = f"\033[33m{status:<{status_width}}\033[0m" # Yellow
|
|
367
|
+
elif status == "attached":
|
|
368
|
+
status_display = f"\033[35m{status:<{status_width}}\033[0m" # Purple
|
|
369
|
+
elif status == "error":
|
|
370
|
+
status_display = (
|
|
371
|
+
f"\033[31m{status:<{status_width}}\033[0m" # Red for error
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
status_display = (
|
|
375
|
+
f"{status:<{status_width}}" # No color for other states
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Format Attached To column
|
|
379
|
+
if attached_to:
|
|
380
|
+
instance_id = attached_to
|
|
381
|
+
engine_name = engines_map.get(instance_id, "unknown")
|
|
382
|
+
# Engine name in white (no color)
|
|
383
|
+
attached_display = f"{engine_name:<{attached_width}}"
|
|
384
|
+
else:
|
|
385
|
+
attached_display = f"{'-':<{attached_width}}"
|
|
386
|
+
|
|
387
|
+
# Color the studio ID (grey)
|
|
388
|
+
studio_id_display = f"\033[90m{studio_id:<{id_width}}\033[0m"
|
|
389
|
+
|
|
390
|
+
click.echo(
|
|
391
|
+
f"│ {user_display}│ {status_display}│ {attached_display}│ {size:<{size_width}}│ {studio_id_display}│"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Table bottom border
|
|
395
|
+
click.echo(
|
|
396
|
+
"╰"
|
|
397
|
+
+ "─" * (user_width + 1)
|
|
398
|
+
+ "┴"
|
|
399
|
+
+ "─" * (status_width + 1)
|
|
400
|
+
+ "┴"
|
|
401
|
+
+ "─" * (attached_width + 1)
|
|
402
|
+
+ "┴"
|
|
403
|
+
+ "─" * (size_width + 1)
|
|
404
|
+
+ "┴"
|
|
405
|
+
+ "─" * (id_width + 1)
|
|
406
|
+
+ "╯"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
click.echo(f"Total: {len(studios)}\n")
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
413
|
+
raise click.Abort()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ============================================================================
|
|
417
|
+
# Attachment
|
|
418
|
+
# ============================================================================
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@studio_cli.command("attach")
|
|
422
|
+
@click.argument("engine_name_or_id")
|
|
423
|
+
@click.option(
|
|
424
|
+
"--user",
|
|
425
|
+
default=None,
|
|
426
|
+
help="User whose studio to attach (defaults to current user, use for testing/admin)",
|
|
427
|
+
)
|
|
428
|
+
@click.option(
|
|
429
|
+
"--env",
|
|
430
|
+
default=None,
|
|
431
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
432
|
+
)
|
|
433
|
+
def attach_studio(engine_name_or_id: str, user: Optional[str], env: Optional[str]):
|
|
434
|
+
"""Attach your studio to an engine with progress tracking."""
|
|
435
|
+
|
|
436
|
+
# Check AWS auth first to provide clear error messages
|
|
437
|
+
check_aws_auth()
|
|
438
|
+
|
|
439
|
+
# Auto-detect environment if not specified
|
|
440
|
+
if env is None:
|
|
441
|
+
env = detect_aws_environment()
|
|
442
|
+
|
|
443
|
+
client = StudioManagerClient(environment=env)
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
# Get studio (for current user or specified user)
|
|
447
|
+
if user is None:
|
|
448
|
+
studio = client.get_my_studio()
|
|
449
|
+
if not studio:
|
|
450
|
+
click.echo("✗ You don't have a studio yet. Create one with:", err=True)
|
|
451
|
+
click.echo(" dh studio create")
|
|
452
|
+
raise click.Abort()
|
|
453
|
+
else:
|
|
454
|
+
# Get studio by user - list all and filter
|
|
455
|
+
result = client.list_studios()
|
|
456
|
+
studios = result.get("studios", [])
|
|
457
|
+
user_studios = [s for s in studios if s.get("user") == user]
|
|
458
|
+
if not user_studios:
|
|
459
|
+
click.echo(f"✗ User '{user}' doesn't have a studio", err=True)
|
|
460
|
+
raise click.Abort()
|
|
461
|
+
studio = user_studios[0]
|
|
462
|
+
|
|
463
|
+
studio_id = studio["studio_id"]
|
|
464
|
+
|
|
465
|
+
if studio["status"] != "available":
|
|
466
|
+
click.echo(
|
|
467
|
+
f"✗ Studio is not available (status: {studio['status']})", err=True
|
|
468
|
+
)
|
|
469
|
+
raise click.Abort()
|
|
470
|
+
|
|
471
|
+
# Resolve engine name to ID
|
|
472
|
+
engine = client.get_engine_by_name(engine_name_or_id)
|
|
473
|
+
if not engine:
|
|
474
|
+
engine = {"instance_id": engine_name_or_id, "name": engine_name_or_id}
|
|
475
|
+
|
|
476
|
+
engine_id = engine["instance_id"]
|
|
477
|
+
engine_name = engine.get("name", engine_id)
|
|
478
|
+
|
|
479
|
+
click.echo(f"📎 Attaching studio to {engine_name}...")
|
|
480
|
+
|
|
481
|
+
# Initiate attachment
|
|
482
|
+
result = client.attach_studio(
|
|
483
|
+
studio_id=studio_id, engine_id=engine_id, user=studio["user"]
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if "error" in result:
|
|
487
|
+
click.echo(f"✗ Error: {result['error']}", err=True)
|
|
488
|
+
raise click.Abort()
|
|
489
|
+
|
|
490
|
+
operation_id = result["operation_id"]
|
|
491
|
+
|
|
492
|
+
# Poll for progress
|
|
493
|
+
click.echo(f"\n⏳ Attachment in progress...\n")
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
final_status = wait_with_progress(
|
|
497
|
+
status_func=lambda: client.get_attachment_progress(operation_id),
|
|
498
|
+
is_complete_func=lambda s: s.get("status") == "completed",
|
|
499
|
+
label="Progress",
|
|
500
|
+
timeout_seconds=180,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
click.echo(f"\n✓ Studio attached successfully!")
|
|
504
|
+
click.echo(f"\nYour files are now available at:")
|
|
505
|
+
click.echo(f" /studios/{studio['user']}/")
|
|
506
|
+
click.echo(f"\nConnect with:")
|
|
507
|
+
click.echo(f" ssh {engine_name}")
|
|
508
|
+
|
|
509
|
+
except Exception as e:
|
|
510
|
+
# Get final status to show error details
|
|
511
|
+
try:
|
|
512
|
+
final_status = client.get_attachment_progress(operation_id)
|
|
513
|
+
if final_status.get("error"):
|
|
514
|
+
click.echo(
|
|
515
|
+
f"\n✗ Attachment failed: {final_status['error']}", err=True
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Show which step failed
|
|
519
|
+
if final_status.get("steps"):
|
|
520
|
+
failed_step = next(
|
|
521
|
+
(
|
|
522
|
+
s
|
|
523
|
+
for s in reversed(final_status["steps"])
|
|
524
|
+
if s.get("status") == "failed"
|
|
525
|
+
),
|
|
526
|
+
None,
|
|
527
|
+
)
|
|
528
|
+
if failed_step:
|
|
529
|
+
click.echo(f"Failed at step: {failed_step['name']}")
|
|
530
|
+
if failed_step.get("error"):
|
|
531
|
+
click.echo(f"Error: {failed_step['error']}")
|
|
532
|
+
except:
|
|
533
|
+
pass
|
|
534
|
+
|
|
535
|
+
raise
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
if "Attachment failed" not in str(e):
|
|
539
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
540
|
+
raise click.Abort()
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@studio_cli.command("detach")
|
|
544
|
+
@click.option(
|
|
545
|
+
"--user",
|
|
546
|
+
default=None,
|
|
547
|
+
help="User whose studio to detach (defaults to current user, use for testing/admin)",
|
|
548
|
+
)
|
|
549
|
+
@click.option(
|
|
550
|
+
"--env",
|
|
551
|
+
default=None,
|
|
552
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
553
|
+
)
|
|
554
|
+
def detach_studio(user: Optional[str], env: Optional[str]):
|
|
555
|
+
"""Detach your studio from its engine."""
|
|
556
|
+
|
|
557
|
+
# Check AWS auth first to provide clear error messages
|
|
558
|
+
check_aws_auth()
|
|
559
|
+
|
|
560
|
+
# Auto-detect environment if not specified
|
|
561
|
+
if env is None:
|
|
562
|
+
env = detect_aws_environment()
|
|
563
|
+
|
|
564
|
+
client = StudioManagerClient(environment=env)
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
# Get studio (for current user or specified user)
|
|
568
|
+
if user is None:
|
|
569
|
+
studio = client.get_my_studio()
|
|
570
|
+
if not studio:
|
|
571
|
+
click.echo("✗ You don't have a studio", err=True)
|
|
572
|
+
raise click.Abort()
|
|
573
|
+
else:
|
|
574
|
+
# Get studio by user - list all and filter
|
|
575
|
+
result = client.list_studios()
|
|
576
|
+
studios = result.get("studios", [])
|
|
577
|
+
user_studios = [s for s in studios if s.get("user") == user]
|
|
578
|
+
if not user_studios:
|
|
579
|
+
click.echo(f"✗ User '{user}' doesn't have a studio", err=True)
|
|
580
|
+
raise click.Abort()
|
|
581
|
+
studio = user_studios[0]
|
|
582
|
+
|
|
583
|
+
if studio["status"] != "attached":
|
|
584
|
+
click.echo(
|
|
585
|
+
f"✗ Studio is not attached (status: {studio['status']})", err=True
|
|
586
|
+
)
|
|
587
|
+
raise click.Abort()
|
|
588
|
+
|
|
589
|
+
studio_id = studio["studio_id"]
|
|
590
|
+
|
|
591
|
+
click.echo(f"Detaching studio {studio_id}...")
|
|
592
|
+
|
|
593
|
+
result = client.detach_studio(studio_id)
|
|
594
|
+
|
|
595
|
+
if "error" in result:
|
|
596
|
+
click.echo(f"✗ Error: {result['error']}", err=True)
|
|
597
|
+
raise click.Abort()
|
|
598
|
+
|
|
599
|
+
click.echo(f"✓ Studio detached")
|
|
600
|
+
|
|
601
|
+
except Exception as e:
|
|
602
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
603
|
+
raise click.Abort()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# ============================================================================
|
|
607
|
+
# Maintenance
|
|
608
|
+
# ============================================================================
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@studio_cli.command("resize")
|
|
612
|
+
@click.option("--size", "-s", required=True, type=int, help="New size in GB")
|
|
613
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
614
|
+
@click.option(
|
|
615
|
+
"--user",
|
|
616
|
+
default=None,
|
|
617
|
+
help="User whose studio to resize (defaults to current user, use for testing/admin)",
|
|
618
|
+
)
|
|
619
|
+
@click.option(
|
|
620
|
+
"--env",
|
|
621
|
+
default=None,
|
|
622
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
623
|
+
)
|
|
624
|
+
def resize_studio(size: int, yes: bool, user: Optional[str], env: Optional[str]):
|
|
625
|
+
"""Resize your studio volume (requires detachment)."""
|
|
626
|
+
|
|
627
|
+
# Check AWS auth and auto-detect environment if not specified
|
|
628
|
+
check_aws_auth()
|
|
629
|
+
|
|
630
|
+
if env is None:
|
|
631
|
+
env = detect_aws_environment()
|
|
632
|
+
|
|
633
|
+
client = StudioManagerClient(environment=env)
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
# Get studio (for current user or specified user)
|
|
637
|
+
if user is None:
|
|
638
|
+
studio = client.get_my_studio()
|
|
639
|
+
if not studio:
|
|
640
|
+
click.echo("✗ You don't have a studio", err=True)
|
|
641
|
+
raise click.Abort()
|
|
642
|
+
else:
|
|
643
|
+
# Get studio by user - list all and filter
|
|
644
|
+
result = client.list_studios()
|
|
645
|
+
studios = result.get("studios", [])
|
|
646
|
+
user_studios = [s for s in studios if s.get("user") == user]
|
|
647
|
+
if not user_studios:
|
|
648
|
+
click.echo(f"✗ User '{user}' doesn't have a studio", err=True)
|
|
649
|
+
raise click.Abort()
|
|
650
|
+
studio = user_studios[0]
|
|
651
|
+
|
|
652
|
+
studio_id = studio["studio_id"]
|
|
653
|
+
|
|
654
|
+
# Must be detached
|
|
655
|
+
if studio["status"] != "available":
|
|
656
|
+
click.echo(
|
|
657
|
+
f"✗ Studio must be detached first (status: {studio['status']})",
|
|
658
|
+
err=True,
|
|
659
|
+
)
|
|
660
|
+
raise click.Abort()
|
|
661
|
+
|
|
662
|
+
current_size = studio.get("size_gb", 0)
|
|
663
|
+
|
|
664
|
+
if size <= current_size:
|
|
665
|
+
click.echo(
|
|
666
|
+
f"✗ New size ({size}GB) must be larger than current size ({current_size}GB)",
|
|
667
|
+
err=True,
|
|
668
|
+
)
|
|
669
|
+
raise click.Abort()
|
|
670
|
+
|
|
671
|
+
if not yes and not click.confirm(
|
|
672
|
+
f"Resize studio from {current_size}GB to {size}GB?"
|
|
673
|
+
):
|
|
674
|
+
click.echo("Cancelled")
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
result = client.resize_studio(studio_id, size)
|
|
678
|
+
|
|
679
|
+
if "error" in result:
|
|
680
|
+
click.echo(f"✗ Error: {result['error']}", err=True)
|
|
681
|
+
raise click.Abort()
|
|
682
|
+
|
|
683
|
+
click.echo(f"✓ Studio resize initiated: {current_size}GB → {size}GB")
|
|
684
|
+
|
|
685
|
+
except Exception as e:
|
|
686
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
687
|
+
raise click.Abort()
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@studio_cli.command("reset")
|
|
691
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
692
|
+
@click.option(
|
|
693
|
+
"--user",
|
|
694
|
+
default=None,
|
|
695
|
+
help="User whose studio to reset (defaults to current user, use for testing/admin)",
|
|
696
|
+
)
|
|
697
|
+
@click.option(
|
|
698
|
+
"--env",
|
|
699
|
+
default=None,
|
|
700
|
+
help="Environment (dev, sand, prod) - auto-detected if not specified",
|
|
701
|
+
)
|
|
702
|
+
def reset_studio(yes: bool, user: Optional[str], env: Optional[str]):
|
|
703
|
+
"""Reset a stuck studio (admin operation)."""
|
|
704
|
+
|
|
705
|
+
# Check AWS auth and auto-detect environment if not specified
|
|
706
|
+
check_aws_auth()
|
|
707
|
+
|
|
708
|
+
if env is None:
|
|
709
|
+
env = detect_aws_environment()
|
|
710
|
+
|
|
711
|
+
client = StudioManagerClient(environment=env)
|
|
712
|
+
|
|
713
|
+
try:
|
|
714
|
+
# Get studio (for current user or specified user)
|
|
715
|
+
if user is None:
|
|
716
|
+
studio = client.get_my_studio()
|
|
717
|
+
if not studio:
|
|
718
|
+
click.echo("✗ You don't have a studio", err=True)
|
|
719
|
+
raise click.Abort()
|
|
720
|
+
else:
|
|
721
|
+
# Get studio by user - list all and filter
|
|
722
|
+
result = client.list_studios()
|
|
723
|
+
studios = result.get("studios", [])
|
|
724
|
+
user_studios = [s for s in studios if s.get("user") == user]
|
|
725
|
+
if not user_studios:
|
|
726
|
+
click.echo(f"✗ User '{user}' doesn't have a studio", err=True)
|
|
727
|
+
raise click.Abort()
|
|
728
|
+
studio = user_studios[0]
|
|
729
|
+
|
|
730
|
+
studio_id = studio["studio_id"]
|
|
731
|
+
current_status = studio.get("status", "unknown")
|
|
732
|
+
|
|
733
|
+
click.echo(f"Studio: {studio_id}")
|
|
734
|
+
click.echo(f"Current Status: {current_status}")
|
|
735
|
+
|
|
736
|
+
if current_status in ["available", "attached"]:
|
|
737
|
+
click.echo("Studio is not stuck (status is normal)")
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
if not yes and not click.confirm(f"Reset studio status to 'available'?"):
|
|
741
|
+
click.echo("Cancelled")
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
result = client.reset_studio(studio_id)
|
|
745
|
+
|
|
746
|
+
if "error" in result:
|
|
747
|
+
click.echo(f"✗ Error: {result['error']}", err=True)
|
|
748
|
+
raise click.Abort()
|
|
749
|
+
|
|
750
|
+
click.echo(f"✓ Studio reset to 'available' status")
|
|
751
|
+
click.echo(f" Note: Manual cleanup may be required on engines")
|
|
752
|
+
|
|
753
|
+
except Exception as e:
|
|
754
|
+
click.echo(f"✗ Error: {e}", err=True)
|
|
755
|
+
raise click.Abort()
|