dayhoff-tools 1.3.2__py3-none-any.whl → 1.3.4__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/cli/engine_commands.py +89 -49
- {dayhoff_tools-1.3.2.dist-info → dayhoff_tools-1.3.4.dist-info}/METADATA +1 -1
- {dayhoff_tools-1.3.2.dist-info → dayhoff_tools-1.3.4.dist-info}/RECORD +5 -5
- {dayhoff_tools-1.3.2.dist-info → dayhoff_tools-1.3.4.dist-info}/WHEEL +0 -0
- {dayhoff_tools-1.3.2.dist-info → dayhoff_tools-1.3.4.dist-info}/entry_points.txt +0 -0
@@ -3,7 +3,7 @@
|
|
3
3
|
import json
|
4
4
|
import subprocess
|
5
5
|
import sys
|
6
|
-
from datetime import datetime, timedelta
|
6
|
+
from datetime import datetime, timedelta, timezone
|
7
7
|
from pathlib import Path
|
8
8
|
from typing import Dict, List, Optional, Tuple
|
9
9
|
|
@@ -134,11 +134,15 @@ def parse_launch_time(launch_time_str: str) -> datetime:
|
|
134
134
|
]
|
135
135
|
for fmt in formats:
|
136
136
|
try:
|
137
|
-
|
137
|
+
parsed = datetime.strptime(launch_time_str, fmt)
|
138
|
+
# If the format includes 'Z', it's UTC
|
139
|
+
if fmt.endswith('Z'):
|
140
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
141
|
+
return parsed
|
138
142
|
except ValueError:
|
139
143
|
continue
|
140
144
|
# Fallback: assume it's recent
|
141
|
-
return datetime.
|
145
|
+
return datetime.now(timezone.utc)
|
142
146
|
|
143
147
|
|
144
148
|
def format_status(state: str, ready: Optional[bool]) -> str:
|
@@ -330,9 +334,7 @@ def list_engines(
|
|
330
334
|
current_user = check_aws_sso()
|
331
335
|
|
332
336
|
params = {}
|
333
|
-
if
|
334
|
-
params["user"] = current_user
|
335
|
-
elif user:
|
337
|
+
if user:
|
336
338
|
params["user"] = user
|
337
339
|
|
338
340
|
response = make_api_request("GET", "/engines", params=params)
|
@@ -365,7 +367,7 @@ def list_engines(
|
|
365
367
|
total_cost = 0.0
|
366
368
|
for engine in engines:
|
367
369
|
launch_time = parse_launch_time(engine["launch_time"])
|
368
|
-
uptime = datetime.
|
370
|
+
uptime = datetime.now(timezone.utc) - launch_time
|
369
371
|
hourly_cost = HOURLY_COSTS.get(engine["engine_type"], 0)
|
370
372
|
|
371
373
|
if engine["state"].lower() == "running":
|
@@ -420,7 +422,7 @@ def engine_status(
|
|
420
422
|
|
421
423
|
# Calculate costs
|
422
424
|
launch_time = parse_launch_time(engine["launch_time"])
|
423
|
-
uptime = datetime.
|
425
|
+
uptime = datetime.now(timezone.utc) - launch_time
|
424
426
|
hourly_cost = HOURLY_COSTS.get(engine["engine_type"], 0)
|
425
427
|
total_cost = hourly_cost * (uptime.total_seconds() / 3600)
|
426
428
|
|
@@ -552,7 +554,7 @@ def terminate_engine(
|
|
552
554
|
|
553
555
|
# Calculate cost
|
554
556
|
launch_time = parse_launch_time(engine["launch_time"])
|
555
|
-
uptime = datetime.
|
557
|
+
uptime = datetime.now(timezone.utc) - launch_time
|
556
558
|
hourly_cost = HOURLY_COSTS.get(engine["engine_type"], 0)
|
557
559
|
total_cost = hourly_cost * (uptime.total_seconds() / 3600)
|
558
560
|
|
@@ -947,7 +949,7 @@ def get_user_studio(username: str) -> Optional[Dict]:
|
|
947
949
|
return None
|
948
950
|
|
949
951
|
studios = response.json().get("studios", [])
|
950
|
-
user_studios = [s for s in studios if s["
|
952
|
+
user_studios = [s for s in studios if s["user"] == username]
|
951
953
|
|
952
954
|
return user_studios[0] if user_studios else None
|
953
955
|
|
@@ -962,7 +964,7 @@ def create_studio(
|
|
962
964
|
# Check if user already has a studio
|
963
965
|
existing = get_user_studio(username)
|
964
966
|
if existing:
|
965
|
-
console.print(f"[yellow]You already have a studio: {existing['
|
967
|
+
console.print(f"[yellow]You already have a studio: {existing['studio_id']}[/yellow]")
|
966
968
|
return
|
967
969
|
|
968
970
|
console.print(f"Creating {size_gb}GB studio for user [cyan]{username}[/cyan]...")
|
@@ -1003,23 +1005,32 @@ def studio_status():
|
|
1003
1005
|
return
|
1004
1006
|
|
1005
1007
|
# Create status panel
|
1008
|
+
# Format status with colors
|
1009
|
+
status = studio['status']
|
1010
|
+
if status == "in-use":
|
1011
|
+
status_display = "[magenta]attached[/magenta]"
|
1012
|
+
elif status in ["attaching", "detaching"]:
|
1013
|
+
status_display = f"[yellow]{status}[/yellow]"
|
1014
|
+
else:
|
1015
|
+
status_display = f"[green]{status}[/green]"
|
1016
|
+
|
1006
1017
|
status_lines = [
|
1007
|
-
f"[bold]Studio ID:[/bold] {studio['
|
1008
|
-
f"[bold]User:[/bold] {studio['
|
1009
|
-
f"[bold]Status:[/bold] {
|
1010
|
-
f"[bold]Size:[/bold] {studio['
|
1011
|
-
f"[bold]Created:[/bold] {studio['
|
1018
|
+
f"[bold]Studio ID:[/bold] {studio['studio_id']}",
|
1019
|
+
f"[bold]User:[/bold] {studio['user']}",
|
1020
|
+
f"[bold]Status:[/bold] {status_display}",
|
1021
|
+
f"[bold]Size:[/bold] {studio['size_gb']}GB",
|
1022
|
+
f"[bold]Created:[/bold] {studio['creation_date']}",
|
1012
1023
|
]
|
1013
1024
|
|
1014
|
-
if studio.get("
|
1015
|
-
status_lines.append(f"[bold]Attached to:[/bold] {studio['
|
1025
|
+
if studio.get("attached_vm_id"):
|
1026
|
+
status_lines.append(f"[bold]Attached to:[/bold] {studio['attached_vm_id']}")
|
1016
1027
|
|
1017
1028
|
# Try to get engine details
|
1018
1029
|
response = make_api_request("GET", "/engines")
|
1019
1030
|
if response.status_code == 200:
|
1020
1031
|
engines = response.json().get("engines", [])
|
1021
1032
|
attached_engine = next(
|
1022
|
-
(e for e in engines if e["instance_id"] == studio["
|
1033
|
+
(e for e in engines if e["instance_id"] == studio["attached_vm_id"]),
|
1023
1034
|
None
|
1024
1035
|
)
|
1025
1036
|
if attached_engine:
|
@@ -1055,19 +1066,19 @@ def attach_studio(
|
|
1055
1066
|
console.print("[red]❌ Failed to create studio[/red]")
|
1056
1067
|
raise typer.Exit(1)
|
1057
1068
|
studio = response.json()
|
1058
|
-
studio["
|
1069
|
+
studio["studio_id"] = studio["studio_id"] # Normalize key
|
1059
1070
|
else:
|
1060
1071
|
raise typer.Exit(0)
|
1061
1072
|
|
1062
1073
|
# Check if already attached
|
1063
|
-
if studio.get("
|
1074
|
+
if studio.get("status") == "in-use":
|
1064
1075
|
console.print(
|
1065
|
-
f"[yellow]Studio is already attached to {studio.get('
|
1076
|
+
f"[yellow]Studio is already attached to {studio.get('attached_vm_id')}[/yellow]"
|
1066
1077
|
)
|
1067
1078
|
if not Confirm.ask("Detach and reattach to new engine?"):
|
1068
1079
|
return
|
1069
1080
|
# Detach first
|
1070
|
-
response = make_api_request("POST", f"/studios/{studio['
|
1081
|
+
response = make_api_request("POST", f"/studios/{studio['studio_id']}/detach")
|
1071
1082
|
if response.status_code != 200:
|
1072
1083
|
console.print("[red]❌ Failed to detach studio[/red]")
|
1073
1084
|
raise typer.Exit(1)
|
@@ -1113,7 +1124,7 @@ def attach_studio(
|
|
1113
1124
|
|
1114
1125
|
response = make_api_request(
|
1115
1126
|
"POST",
|
1116
|
-
f"/studios/{studio['
|
1127
|
+
f"/studios/{studio['studio_id']}/attach",
|
1117
1128
|
json_data={
|
1118
1129
|
"vm_id": engine["instance_id"],
|
1119
1130
|
"user": username,
|
@@ -1146,13 +1157,13 @@ def detach_studio():
|
|
1146
1157
|
console.print("[yellow]You don't have a studio.[/yellow]")
|
1147
1158
|
return
|
1148
1159
|
|
1149
|
-
if studio.get("
|
1160
|
+
if studio.get("status") != "in-use":
|
1150
1161
|
console.print("[yellow]Your studio is not attached to any engine.[/yellow]")
|
1151
1162
|
return
|
1152
1163
|
|
1153
|
-
console.print(f"Detaching studio from {studio.get('
|
1164
|
+
console.print(f"Detaching studio from {studio.get('attached_vm_id')}...")
|
1154
1165
|
|
1155
|
-
response = make_api_request("POST", f"/studios/{studio['
|
1166
|
+
response = make_api_request("POST", f"/studios/{studio['studio_id']}/detach")
|
1156
1167
|
|
1157
1168
|
if response.status_code == 200:
|
1158
1169
|
console.print(f"[green]✓ Studio detached successfully![/green]")
|
@@ -1172,8 +1183,8 @@ def delete_studio():
|
|
1172
1183
|
return
|
1173
1184
|
|
1174
1185
|
console.print("[red]⚠️ WARNING: This will permanently delete your studio and all data![/red]")
|
1175
|
-
console.print(f"Studio ID: {studio['
|
1176
|
-
console.print(f"Size: {studio['
|
1186
|
+
console.print(f"Studio ID: {studio['studio_id']}")
|
1187
|
+
console.print(f"Size: {studio['size_gb']}GB")
|
1177
1188
|
|
1178
1189
|
# Multiple confirmations
|
1179
1190
|
if not Confirm.ask("\nAre you sure you want to delete your studio?"):
|
@@ -1191,7 +1202,7 @@ def delete_studio():
|
|
1191
1202
|
console.print("Deletion cancelled.")
|
1192
1203
|
return
|
1193
1204
|
|
1194
|
-
response = make_api_request("DELETE", f"/studios/{studio['
|
1205
|
+
response = make_api_request("DELETE", f"/studios/{studio['studio_id']}")
|
1195
1206
|
|
1196
1207
|
if response.status_code == 200:
|
1197
1208
|
console.print(f"[green]✓ Studio deleted successfully![/green]")
|
@@ -1212,13 +1223,17 @@ def list_studios(
|
|
1212
1223
|
if response.status_code == 200:
|
1213
1224
|
studios = response.json().get("studios", [])
|
1214
1225
|
|
1215
|
-
if not all_users:
|
1216
|
-
studios = [s for s in studios if s["UserID"] == username]
|
1217
|
-
|
1218
1226
|
if not studios:
|
1219
1227
|
console.print("No studios found.")
|
1220
1228
|
return
|
1221
1229
|
|
1230
|
+
# Get all engines to map instance IDs to names
|
1231
|
+
engines_response = make_api_request("GET", "/engines")
|
1232
|
+
engines = {}
|
1233
|
+
if engines_response.status_code == 200:
|
1234
|
+
for engine in engines_response.json().get("engines", []):
|
1235
|
+
engines[engine["instance_id"]] = engine["name"]
|
1236
|
+
|
1222
1237
|
# Create table
|
1223
1238
|
table = Table(title="Studios", box=box.ROUNDED)
|
1224
1239
|
table.add_column("Studio ID", style="cyan")
|
@@ -1229,14 +1244,39 @@ def list_studios(
|
|
1229
1244
|
table.add_column("Created")
|
1230
1245
|
|
1231
1246
|
for studio in studios:
|
1232
|
-
|
1247
|
+
# Change status display
|
1248
|
+
if studio["status"] == "in-use":
|
1249
|
+
status_display = "[magenta]attached[/magenta]"
|
1250
|
+
elif studio["status"] in ["attaching", "detaching"]:
|
1251
|
+
status_display = "[yellow]" + studio["status"] + "[/yellow]"
|
1252
|
+
else:
|
1253
|
+
status_display = "[green]available[/green]"
|
1254
|
+
|
1255
|
+
# Format attached engine info
|
1256
|
+
attached_to = "-"
|
1257
|
+
if studio.get("attached_vm_id"):
|
1258
|
+
vm_id = studio["attached_vm_id"]
|
1259
|
+
engine_name = engines.get(vm_id, "unknown")
|
1260
|
+
attached_to = f"{engine_name} ({vm_id})"
|
1261
|
+
|
1262
|
+
# Format creation date (remove microseconds and timezone info)
|
1263
|
+
created = studio["creation_date"]
|
1264
|
+
try:
|
1265
|
+
# Parse and reformat to just show date and time
|
1266
|
+
if 'T' in created:
|
1267
|
+
created_dt = datetime.fromisoformat(created.replace('Z', '+00:00'))
|
1268
|
+
created = created_dt.strftime("%Y-%m-%d %H:%M")
|
1269
|
+
except:
|
1270
|
+
# If parsing fails, just truncate
|
1271
|
+
created = created.split('T')[0] if 'T' in created else created[:16]
|
1272
|
+
|
1233
1273
|
table.add_row(
|
1234
|
-
studio["
|
1235
|
-
studio["
|
1236
|
-
|
1237
|
-
f"{studio['
|
1238
|
-
|
1239
|
-
|
1274
|
+
studio["studio_id"],
|
1275
|
+
studio["user"],
|
1276
|
+
status_display,
|
1277
|
+
f"{studio['size_gb']}GB",
|
1278
|
+
attached_to,
|
1279
|
+
created,
|
1240
1280
|
)
|
1241
1281
|
|
1242
1282
|
console.print(table)
|
@@ -1256,9 +1296,9 @@ def reset_studio():
|
|
1256
1296
|
return
|
1257
1297
|
|
1258
1298
|
console.print(f"[yellow]⚠️ This will force-reset your studio state[/yellow]")
|
1259
|
-
console.print(f"Current status: {studio['
|
1260
|
-
if studio.get("
|
1261
|
-
console.print(f"Listed as attached to: {studio['
|
1299
|
+
console.print(f"Current status: {studio['status']}")
|
1300
|
+
if studio.get("attached_vm_id"):
|
1301
|
+
console.print(f"Listed as attached to: {studio['attached_vm_id']}")
|
1262
1302
|
|
1263
1303
|
if not Confirm.ask("\nReset studio state?"):
|
1264
1304
|
console.print("Reset cancelled.")
|
@@ -1273,7 +1313,7 @@ def reset_studio():
|
|
1273
1313
|
try:
|
1274
1314
|
# Check if volume is actually attached
|
1275
1315
|
ec2 = boto3.client("ec2", region_name="us-east-1")
|
1276
|
-
volumes = ec2.describe_volumes(VolumeIds=[studio["
|
1316
|
+
volumes = ec2.describe_volumes(VolumeIds=[studio["studio_id"]])
|
1277
1317
|
|
1278
1318
|
if volumes["Volumes"]:
|
1279
1319
|
volume = volumes["Volumes"][0]
|
@@ -1284,19 +1324,19 @@ def reset_studio():
|
|
1284
1324
|
)
|
1285
1325
|
if Confirm.ask("Force-detach the volume?"):
|
1286
1326
|
ec2.detach_volume(
|
1287
|
-
VolumeId=studio["
|
1327
|
+
VolumeId=studio["studio_id"],
|
1288
1328
|
InstanceId=attachments[0]["InstanceId"],
|
1289
1329
|
Force=True,
|
1290
1330
|
)
|
1291
1331
|
console.print("Waiting for volume to detach...")
|
1292
1332
|
waiter = ec2.get_waiter("volume_available")
|
1293
|
-
waiter.wait(VolumeIds=[studio["
|
1333
|
+
waiter.wait(VolumeIds=[studio["studio_id"]])
|
1294
1334
|
|
1295
1335
|
# Reset in DynamoDB
|
1296
1336
|
table.update_item(
|
1297
|
-
Key={"
|
1298
|
-
UpdateExpression="SET #status = :status,
|
1299
|
-
ExpressionAttributeNames={"#status": "
|
1337
|
+
Key={"studio_id": studio["studio_id"]},
|
1338
|
+
UpdateExpression="SET #status = :status, attached_vm_id = :vm_id, attached_device = :device",
|
1339
|
+
ExpressionAttributeNames={"#status": "status"},
|
1300
1340
|
ExpressionAttributeValues={
|
1301
1341
|
":status": "available",
|
1302
1342
|
":vm_id": None,
|
@@ -3,7 +3,7 @@ dayhoff_tools/chemistry/standardizer.py,sha256=uMn7VwHnx02nc404eO6fRuS4rsl4dvSPf
|
|
3
3
|
dayhoff_tools/chemistry/utils.py,sha256=jt-7JgF-GeeVC421acX-bobKbLU_X94KNOW24p_P-_M,2257
|
4
4
|
dayhoff_tools/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
dayhoff_tools/cli/cloud_commands.py,sha256=33qcWLmq-FwEXMdL3F0OHm-5Stlh2r65CldyEZgQ1no,40904
|
6
|
-
dayhoff_tools/cli/engine_commands.py,sha256=
|
6
|
+
dayhoff_tools/cli/engine_commands.py,sha256=w0wzs4aQUdzeyAyhHH9lHxpJEG8acrbguQ4Rd0u5bA4,48179
|
7
7
|
dayhoff_tools/cli/main.py,sha256=rgeEHD9lJ8SBCR34BTLb7gVInHUUdmEBNXAJnq5yEU4,4795
|
8
8
|
dayhoff_tools/cli/swarm_commands.py,sha256=5EyKj8yietvT5lfoz8Zx0iQvVaNgc3SJX1z2zQR6o6M,5614
|
9
9
|
dayhoff_tools/cli/utility_commands.py,sha256=qs8vH9TBFHsOPC3X8cU3qZigM3dDn-2Ytq4o_F2WubU,27874
|
@@ -27,7 +27,7 @@ dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJq
|
|
27
27
|
dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
|
28
28
|
dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
|
29
29
|
dayhoff_tools/warehouse.py,sha256=8YbnQ--usrEgDQGfvpV4MrMji55A0rq2hZaOgFGh6ag,15896
|
30
|
-
dayhoff_tools-1.3.
|
31
|
-
dayhoff_tools-1.3.
|
32
|
-
dayhoff_tools-1.3.
|
33
|
-
dayhoff_tools-1.3.
|
30
|
+
dayhoff_tools-1.3.4.dist-info/METADATA,sha256=YZw82MtQpm4aWNzOfEl62Iov6ZRXtw6LfZ43eI9psKE,2842
|
31
|
+
dayhoff_tools-1.3.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
32
|
+
dayhoff_tools-1.3.4.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
|
33
|
+
dayhoff_tools-1.3.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|