dayhoff-tools 1.9.12__py3-none-any.whl → 1.9.14__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.
@@ -1,5 +1,7 @@
1
1
  """Engine and Studio management commands for DHT CLI."""
2
2
 
3
+ from typing import Optional
4
+
3
5
  import typer
4
6
 
5
7
  # Initialize Typer apps
@@ -9,6 +11,7 @@ studio_app = typer.Typer(help="Manage persistent development studios.")
9
11
  # Use lazy loading pattern similar to main.py swarm commands
10
12
  # Import functions only when commands are actually called
11
13
 
14
+
12
15
  # Engine commands
13
16
  @engine_app.command("launch")
14
17
  def launch_engine_cmd(
@@ -34,6 +37,7 @@ def launch_engine_cmd(
34
37
  ):
35
38
  """Launch a new engine instance."""
36
39
  from .engine_core import launch_engine
40
+
37
41
  return launch_engine(name, engine_type, user, boot_disk_size, availability_zone)
38
42
 
39
43
 
@@ -52,17 +56,23 @@ def list_engines_cmd(
52
56
  ):
53
57
  """List engines (shows all engines by default)."""
54
58
  from .engine_core import list_engines
59
+
55
60
  return list_engines(user, running_only, stopped_only, detailed)
56
61
 
57
62
 
58
63
  @engine_app.command("status")
59
64
  def engine_status_cmd(
60
65
  name_or_id: str = typer.Argument(help="Engine name or instance ID"),
61
- detailed: bool = typer.Option(False, "--detailed", "-d", help="Show detailed status (slower)"),
62
- show_log: bool = typer.Option(False, "--show-log", help="Show bootstrap log (requires --detailed)"),
66
+ detailed: bool = typer.Option(
67
+ False, "--detailed", "-d", help="Show detailed status (slower)"
68
+ ),
69
+ show_log: bool = typer.Option(
70
+ False, "--show-log", help="Show bootstrap log (requires --detailed)"
71
+ ),
63
72
  ):
64
73
  """Show engine status and information."""
65
74
  from .engine_core import engine_status
75
+
66
76
  return engine_status(name_or_id, detailed, show_log)
67
77
 
68
78
 
@@ -72,6 +82,7 @@ def start_engine_cmd(
72
82
  ):
73
83
  """Start a stopped engine."""
74
84
  from .engine_lifecycle import start_engine
85
+
75
86
  return start_engine(name_or_id)
76
87
 
77
88
 
@@ -84,6 +95,7 @@ def stop_engine_cmd(
84
95
  ):
85
96
  """Stop an engine."""
86
97
  from .engine_lifecycle import stop_engine
98
+
87
99
  return stop_engine(name_or_id, force)
88
100
 
89
101
 
@@ -93,6 +105,7 @@ def terminate_engine_cmd(
93
105
  ):
94
106
  """Permanently terminate an engine."""
95
107
  from .engine_lifecycle import terminate_engine
108
+
96
109
  return terminate_engine(name_or_id)
97
110
 
98
111
 
@@ -110,6 +123,7 @@ def ssh_engine_cmd(
110
123
  ):
111
124
  """Connect to an engine via SSH."""
112
125
  from .engine_management import ssh_engine
126
+
113
127
  return ssh_engine(name_or_id, admin, idle_timeout)
114
128
 
115
129
 
@@ -127,6 +141,7 @@ def config_ssh_cmd(
127
141
  ):
128
142
  """Update SSH config with available engines."""
129
143
  from .engine_management import config_ssh
144
+
130
145
  return config_ssh(clean, all_engines, admin)
131
146
 
132
147
 
@@ -145,6 +160,7 @@ def resize_engine_cmd(
145
160
  ):
146
161
  """Resize an engine's boot disk."""
147
162
  from .engine_management import resize_engine
163
+
148
164
  return resize_engine(name_or_id, size, online, force)
149
165
 
150
166
 
@@ -156,6 +172,7 @@ def create_ami_cmd(
156
172
  ):
157
173
  """Create a 'Golden AMI' from a running engine."""
158
174
  from .engine_management import create_ami
175
+
159
176
  return create_ami(name_or_id)
160
177
 
161
178
 
@@ -169,19 +186,24 @@ def coffee_cmd(
169
186
  ):
170
187
  """Pour ☕ for an engine: keeps it awake for the given duration (or cancel)."""
171
188
  from .engine_maintenance import coffee
189
+
172
190
  return coffee(name_or_id, duration, cancel)
173
191
 
174
192
 
175
193
  @engine_app.command("idle")
176
194
  def idle_timeout_cmd_wrapper(
177
195
  name_or_id: str = typer.Argument(help="Engine name or instance ID"),
178
- set: str = typer.Option(
196
+ set: Optional[str] = typer.Option(
179
197
  None, "--set", "-s", help="New timeout (e.g., 2h30m, 45m)"
180
198
  ),
199
+ slack: Optional[str] = typer.Option(
200
+ None, "--slack", help="Set Slack notifications: none, default, all"
201
+ ),
181
202
  ):
182
- """Show or set the engine idle-detector timeout."""
203
+ """Show or set engine idle-detector settings."""
183
204
  from .engine_maintenance import idle_timeout_cmd
184
- return idle_timeout_cmd(name_or_id, set)
205
+
206
+ return idle_timeout_cmd(name_or_id=name_or_id, set=set, slack=slack)
185
207
 
186
208
 
187
209
  @engine_app.command("debug")
@@ -190,6 +212,7 @@ def debug_engine_cmd(
190
212
  ):
191
213
  """Debug engine bootstrap status and files."""
192
214
  from .engine_maintenance import debug_engine
215
+
193
216
  return debug_engine(name_or_id)
194
217
 
195
218
 
@@ -199,6 +222,7 @@ def repair_engine_cmd(
199
222
  ):
200
223
  """Repair an engine that's stuck in a bad state (e.g., after GAMI creation)."""
201
224
  from .engine_maintenance import repair_engine
225
+
202
226
  return repair_engine(name_or_id)
203
227
 
204
228
 
@@ -209,6 +233,7 @@ def create_studio_cmd(
209
233
  ):
210
234
  """Create a new studio for the current user."""
211
235
  from .studio_commands import create_studio
236
+
212
237
  return create_studio(size_gb)
213
238
 
214
239
 
@@ -220,6 +245,7 @@ def studio_status_cmd(
220
245
  ):
221
246
  """Show status of your studio."""
222
247
  from .studio_commands import studio_status
248
+
223
249
  return studio_status(user)
224
250
 
225
251
 
@@ -232,6 +258,7 @@ def attach_studio_cmd(
232
258
  ):
233
259
  """Attach your studio to an engine."""
234
260
  from .studio_commands import attach_studio
261
+
235
262
  return attach_studio(engine_name_or_id, user)
236
263
 
237
264
 
@@ -243,6 +270,7 @@ def detach_studio_cmd(
243
270
  ):
244
271
  """Detach your studio from its current engine."""
245
272
  from .studio_commands import detach_studio
273
+
246
274
  return detach_studio(user)
247
275
 
248
276
 
@@ -254,6 +282,7 @@ def delete_studio_cmd(
254
282
  ):
255
283
  """Delete your studio permanently."""
256
284
  from .studio_commands import delete_studio
285
+
257
286
  return delete_studio(user)
258
287
 
259
288
 
@@ -265,6 +294,7 @@ def list_studios_cmd(
265
294
  ):
266
295
  """List studios."""
267
296
  from .studio_commands import list_studios
297
+
268
298
  return list_studios(all_users)
269
299
 
270
300
 
@@ -276,6 +306,7 @@ def reset_studio_cmd(
276
306
  ):
277
307
  """Reset a stuck studio (admin operation)."""
278
308
  from .studio_commands import reset_studio
309
+
279
310
  return reset_studio(user)
280
311
 
281
312
 
@@ -288,4 +319,5 @@ def resize_studio_cmd(
288
319
  ):
289
320
  """Resize your studio volume (requires detachment)."""
290
321
  from .studio_commands import resize_studio
291
- return resize_studio(size, user)
322
+
323
+ return resize_studio(size, user)
@@ -217,8 +217,12 @@ def list_engines(
217
217
 
218
218
  def engine_status(
219
219
  name_or_id: str = typer.Argument(help="Engine name or instance ID"),
220
- detailed: bool = typer.Option(False, "--detailed", "-d", help="Show detailed status (slower)"),
221
- show_log: bool = typer.Option(False, "--show-log", help="Show bootstrap log (requires --detailed)"),
220
+ detailed: bool = typer.Option(
221
+ False, "--detailed", "-d", help="Show detailed status (slower)"
222
+ ),
223
+ show_log: bool = typer.Option(
224
+ False, "--show-log", help="Show bootstrap log (requires --detailed)"
225
+ ),
222
226
  ):
223
227
  """Show engine status and information."""
224
228
  check_aws_sso()
@@ -477,6 +481,47 @@ def engine_status(
477
481
  except Exception:
478
482
  pass
479
483
 
484
+ # Slack notifications status (detailed view only)
485
+ try:
486
+ ssm = boto3.client("ssm", region_name="us-east-1")
487
+ resp = ssm.send_command(
488
+ InstanceIds=[engine["instance_id"]],
489
+ DocumentName="AWS-RunShellScript",
490
+ Parameters={
491
+ "commands": ["grep '^SLACK_NOTIFY_' /etc/engine.env || true"],
492
+ "executionTimeout": ["10"],
493
+ },
494
+ )
495
+ cid = resp["Command"]["CommandId"]
496
+ time.sleep(1)
497
+ inv = ssm.get_command_invocation(
498
+ CommandId=cid, InstanceId=engine["instance_id"]
499
+ )
500
+ if inv["Status"] == "Success":
501
+ settings_raw = inv["StandardOutputContent"].strip()
502
+ settings = {}
503
+ for line in settings_raw.splitlines():
504
+ if "=" in line:
505
+ key, value = line.split("=", 1)
506
+ settings[key.strip()] = value.strip().lower()
507
+
508
+ status_lines.append("")
509
+ status_lines.append("[bold]Slack Notifications:[/bold]")
510
+
511
+ def _setting_line(label: str, key: str) -> str:
512
+ val = settings.get(key, "false") # Default to false if not set
513
+ status = "[green]on[/green]" if val == "true" else "[dim]off[/dim]"
514
+ return f" - {label:15} {status}"
515
+
516
+ status_lines.append(_setting_line("Idle Start", "SLACK_NOTIFY_IDLE_START"))
517
+ status_lines.append(
518
+ _setting_line("IDE Disconnect", "SLACK_NOTIFY_IDE_DISCONNECT")
519
+ )
520
+ status_lines.append(_setting_line("Warnings", "SLACK_NOTIFY_WARNINGS"))
521
+ status_lines.append(_setting_line("Shutdown", "SLACK_NOTIFY_SHUTDOWN"))
522
+ except Exception:
523
+ pass
524
+
480
525
  # Activity Sensors (show all with YES/no)
481
526
  if idle_detector.get("available"):
482
527
  status_lines.append("")
@@ -564,10 +609,14 @@ def _format_idle_status_display(
564
609
  ):
565
610
  remaining = max(0, int(thresh_v) - int(idle_seconds_v))
566
611
  remaining_mins = remaining // 60
567
- if remaining_mins == 0:
568
- return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: [red]<1m[/red] left[/yellow]"
612
+ remaining_secs = remaining % 60
613
+
614
+ if remaining < 60:
615
+ time_left_str = f"[red]{remaining}s[/red] left"
569
616
  else:
570
- return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: [red]{remaining_mins}m[/red] left[/yellow]"
617
+ time_left_str = f"[red]{remaining_mins}m {remaining_secs}s[/red] left"
618
+
619
+ return f"[yellow]Idle {int(idle_seconds_v)//60}m/{int(thresh_v)//60}m: {time_left_str}[/yellow]"
571
620
  elif isinstance(thresh_v, (int, float)):
572
621
  return f"[yellow]Idle ?/{int(thresh_v)//60}m[/yellow]"
573
622
  else:
@@ -11,12 +11,7 @@ from botocore.exceptions import ClientError
11
11
  from rich.progress import Progress, SpinnerColumn, TextColumn
12
12
  from rich.prompt import Confirm
13
13
 
14
- from .shared import (
15
- check_aws_sso,
16
- console,
17
- make_api_request,
18
- resolve_engine,
19
- )
14
+ from .shared import check_aws_sso, console, make_api_request, resolve_engine
20
15
 
21
16
 
22
17
  def coffee(
@@ -123,9 +118,11 @@ def idle_timeout_cmd(
123
118
  set: Optional[str] = typer.Option(
124
119
  None, "--set", "-s", help="New timeout (e.g., 2h30m, 45m)"
125
120
  ),
126
-
121
+ slack: Optional[str] = typer.Option(
122
+ None, "--slack", help="Set Slack notifications: none, default, all"
123
+ ),
127
124
  ):
128
- """Show or set the engine idle-detector timeout."""
125
+ """Show or set engine idle-detector settings."""
129
126
  check_aws_sso()
130
127
 
131
128
  # Resolve engine
@@ -139,7 +136,56 @@ def idle_timeout_cmd(
139
136
 
140
137
  ssm = boto3.client("ssm", region_name="us-east-1")
141
138
 
142
- if set is None:
139
+ # Handle slack notifications change
140
+ if slack:
141
+ slack = slack.lower()
142
+ if slack not in ["none", "default", "all"]:
143
+ console.print("[red]❌ Invalid slack option. Use: none, default, all[/red]")
144
+ raise typer.Exit(1)
145
+
146
+ console.print(f"Setting Slack notifications to [bold]{slack}[/bold]...")
147
+
148
+ if slack == "none":
149
+ settings = {
150
+ "SLACK_NOTIFY_WARNINGS": "false",
151
+ "SLACK_NOTIFY_IDLE_START": "false",
152
+ "SLACK_NOTIFY_IDE_DISCONNECT": "false",
153
+ "SLACK_NOTIFY_SHUTDOWN": "false",
154
+ }
155
+ elif slack == "default":
156
+ settings = {
157
+ "SLACK_NOTIFY_WARNINGS": "true",
158
+ "SLACK_NOTIFY_IDLE_START": "false",
159
+ "SLACK_NOTIFY_IDE_DISCONNECT": "false",
160
+ "SLACK_NOTIFY_SHUTDOWN": "true",
161
+ }
162
+ else: # all
163
+ settings = {
164
+ "SLACK_NOTIFY_WARNINGS": "true",
165
+ "SLACK_NOTIFY_IDLE_START": "true",
166
+ "SLACK_NOTIFY_IDE_DISCONNECT": "true",
167
+ "SLACK_NOTIFY_SHUTDOWN": "true",
168
+ }
169
+
170
+ commands = []
171
+ for key, value in settings.items():
172
+ # Use a robust sed command that adds the line if it doesn't exist
173
+ commands.append(
174
+ f"grep -q '^{key}=' /etc/engine.env && sudo sed -i 's|^{key}=.*|{key}={value}|' /etc/engine.env || echo '{key}={value}' | sudo tee -a /etc/engine.env > /dev/null"
175
+ )
176
+
177
+ commands.append("sudo systemctl restart engine-idle-detector.service")
178
+
179
+ resp = ssm.send_command(
180
+ InstanceIds=[engine["instance_id"]],
181
+ DocumentName="AWS-RunShellScript",
182
+ Parameters={"commands": commands, "executionTimeout": ["60"]},
183
+ )
184
+ cid = resp["Command"]["CommandId"]
185
+ time.sleep(2) # Give it a moment to process
186
+ console.print(f"[green]✓ Slack notifications updated to '{slack}'[/green]")
187
+
188
+ if set is None and not slack:
143
189
  # Show current timeout setting
144
190
  resp = ssm.send_command(
145
191
  InstanceIds=[engine["instance_id"]],
@@ -164,7 +210,7 @@ def idle_timeout_cmd(
164
210
  console.print("[red]❌ Could not retrieve idle timeout[/red]")
165
211
  return
166
212
 
167
- # ----- set new value -----
213
+ # ----- set new value for timeout -----
168
214
  m = re.match(r"^(?:(\d+)h)?(?:(\d+)m)?$", set)
169
215
  if not m:
170
216
  console.print("[red]❌ Invalid duration format. Use e.g. 2h, 45m, 1h30m[/red]")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dayhoff-tools
3
- Version: 1.9.12
3
+ Version: 1.9.14
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,10 +3,10 @@ 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/__init__.py,sha256=CGJ2blhWIIEsVb8HoLibZjSlMFRTSYZOO4zDQTtY3SY,9300
7
- dayhoff_tools/cli/engine/engine_core.py,sha256=IEU2m93qArFx-EdVHnepWKvLwlNUAcT7ytIAqeOY6a0,25147
6
+ dayhoff_tools/cli/engine/__init__.py,sha256=RE45X2IPCNUvQp6_OHI2lbjwoqexkH3roUPccIHkeJo,9540
7
+ dayhoff_tools/cli/engine/engine_core.py,sha256=ItfqW3ZWoHEWLOgtWPYT-SO1idxk5RBBCIe8w9xxf3w,26959
8
8
  dayhoff_tools/cli/engine/engine_lifecycle.py,sha256=_Dk-EZs_qbm8APdOuGOuxhlbK6RgkkoLk2nrwKoo1-A,4519
9
- dayhoff_tools/cli/engine/engine_maintenance.py,sha256=Vz4FpbM0eyfl9tTM6Q8z0ZzS2Ug5gAE-uKVbqBHkznU,13761
9
+ dayhoff_tools/cli/engine/engine_maintenance.py,sha256=xZgEgpjwTDI2RpoN7P6E7MEGiIA8RJpb0MkCT9u2R14,15889
10
10
  dayhoff_tools/cli/engine/engine_management.py,sha256=s_H3FtMlKsdfzR8pwV-j2W2QX-Fypkqj2kPC0aTqC1A,19072
11
11
  dayhoff_tools/cli/engine/shared.py,sha256=Ecx6I1jtzmxQDn3BezKpgpQ4SJeZf4SZjUCLg-67p80,16844
12
12
  dayhoff_tools/cli/engine/studio_commands.py,sha256=VwTQujz32-uMcYusDRE73SdzRpgvIkv7ZAF4zRv6AzA,30266
@@ -33,7 +33,7 @@ dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJq
33
33
  dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
34
34
  dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
35
35
  dayhoff_tools/warehouse.py,sha256=UETBtZD3r7WgvURqfGbyHlT7cxoiVq8isjzMuerKw8I,24475
36
- dayhoff_tools-1.9.12.dist-info/METADATA,sha256=EkiO1dejnv9KxAs8X7ycLdfGUlph70tyIeNPWQc-12U,2915
37
- dayhoff_tools-1.9.12.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
38
- dayhoff_tools-1.9.12.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
39
- dayhoff_tools-1.9.12.dist-info/RECORD,,
36
+ dayhoff_tools-1.9.14.dist-info/METADATA,sha256=EewV69EQMTMaa0DUiq2t7J9w6WzsTv7Qd7Pz0Jcjg-w,2915
37
+ dayhoff_tools-1.9.14.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
38
+ dayhoff_tools-1.9.14.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
39
+ dayhoff_tools-1.9.14.dist-info/RECORD,,