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.
@@ -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
- return datetime.strptime(launch_time_str, fmt)
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.utcnow()
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 not all_users and not user:
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.utcnow() - launch_time
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.utcnow() - launch_time
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.utcnow() - launch_time
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["UserID"] == username]
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['StudioID']}[/yellow]")
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['StudioID']}",
1008
- f"[bold]User:[/bold] {studio['UserID']}",
1009
- f"[bold]Status:[/bold] {studio['Status']}",
1010
- f"[bold]Size:[/bold] {studio['SizeGB']}GB",
1011
- f"[bold]Created:[/bold] {studio['CreationDate']}",
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("AttachedVMID"):
1015
- status_lines.append(f"[bold]Attached to:[/bold] {studio['AttachedVMID']}")
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["AttachedVMID"]),
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["StudioID"] = studio["studio_id"] # Normalize key
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("Status") == "in-use":
1074
+ if studio.get("status") == "in-use":
1064
1075
  console.print(
1065
- f"[yellow]Studio is already attached to {studio.get('AttachedVMID')}[/yellow]"
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['StudioID']}/detach")
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['StudioID']}/attach",
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("Status") != "in-use":
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('AttachedVMID')}...")
1164
+ console.print(f"Detaching studio from {studio.get('attached_vm_id')}...")
1154
1165
 
1155
- response = make_api_request("POST", f"/studios/{studio['StudioID']}/detach")
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['StudioID']}")
1176
- console.print(f"Size: {studio['SizeGB']}GB")
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['StudioID']}")
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
- status_color = "green" if studio["Status"] == "available" else "yellow"
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["StudioID"],
1235
- studio["UserID"],
1236
- f"[{status_color}]{studio['Status']}[/{status_color}]",
1237
- f"{studio['SizeGB']}GB",
1238
- studio.get("AttachedVMID", "-"),
1239
- studio["CreationDate"],
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['Status']}")
1260
- if studio.get("AttachedVMID"):
1261
- console.print(f"Listed as attached to: {studio['AttachedVMID']}")
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["StudioID"]])
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["StudioID"],
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["StudioID"]])
1333
+ waiter.wait(VolumeIds=[studio["studio_id"]])
1294
1334
 
1295
1335
  # Reset in DynamoDB
1296
1336
  table.update_item(
1297
- Key={"StudioID": studio["StudioID"]},
1298
- UpdateExpression="SET #status = :status, AttachedVMID = :vm_id, AttachedDevice = :device",
1299
- ExpressionAttributeNames={"#status": "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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dayhoff-tools
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: Common tools for all the repos at Dayhoff Labs
5
5
  Author: Daniel Martin-Alarcon
6
6
  Author-email: dma@dayhofflabs.com
@@ -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=zHyisrYz0kgm_EXFIRqSR10jPPXrBmhMpZnN296x86U,46408
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.2.dist-info/METADATA,sha256=i0AQJh4nEsStInWpyTviptfaOltjscM-MObda7o0vlI,2842
31
- dayhoff_tools-1.3.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
- dayhoff_tools-1.3.2.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
33
- dayhoff_tools-1.3.2.dist-info/RECORD,,
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,,