dayhoff-tools 1.12.31__py3-none-any.whl → 1.12.33__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.
@@ -321,15 +321,26 @@ dh engine2 list --env sand
321
321
 
322
322
  **Output:**
323
323
  ```
324
- Name Instance ID Type State
325
- --------------------------------------------------------------------------------
326
- alice-work i-0123456789abcdef0 cpu running
327
- bob-training i-0fedcba987654321 a10g running
328
- batch-worker i-0abc123def456789 cpumax stopped
324
+ Engines for AWS Account dev
325
+
326
+ ┌──────────────┬─────────────┬─────────────┬─────────────┬─────────────────────┐
327
+ │ Name │ State │ User │ Type │ Instance ID │
328
+ ├──────────────┼─────────────┼─────────────┼─────────────┼─────────────────────┤
329
+ │ alice-work │ running │ alice │ cpu │ i-0123456789abcdef0 │
330
+ │ bob-training │ running │ bob │ a10g │ i-0fedcba987654321 │
331
+ │ batch-worker │ stopped │ charlie │ cpumax │ i-0abc123def456789 │
332
+ └──────────────┴─────────────┴─────────────┴─────────────┴─────────────────────┘
329
333
 
330
334
  Total: 3 engine(s)
331
335
  ```
332
336
 
337
+ **Formatting:**
338
+ - Full table borders with Unicode box-drawing characters
339
+ - Engine names are displayed in blue
340
+ - State is color-coded: green for "running", yellow for "starting/stopping", grey for "stopped"
341
+ - Instance IDs are displayed in grey
342
+ - Name column width adjusts dynamically to fit the longest engine name
343
+
333
344
  ---
334
345
 
335
346
  ### Access
@@ -744,15 +755,29 @@ dh studio2 list
744
755
 
745
756
  **Output:**
746
757
  ```
747
- User Studio ID Size Status
748
- ---------------------------------------------------------------------------
749
- alice vol-0123456789abcdef0 100GB attached
750
- bob vol-0fedcba987654321 200GB available
751
- carol vol-0abc123def456789 150GB available
758
+ Studios for AWS Account dev
759
+
760
+ ┌────────┬──────────────┬──────────────┬───────────┬───────────────────────────┐
761
+ │ User │ Status │ Attached To │ Size │ Studio ID │
762
+ ├────────┼──────────────┼──────────────┼───────────┼───────────────────────────┤
763
+ │ alice │ attached │ alice-work │ 100GB │ vol-0123456789abcdef0 │
764
+ │ bob │ available │ - │ 200GB │ vol-0fedcba987654321 │
765
+ │ carol │ attaching │ carol-gpu │ 150GB │ vol-0abc123def456789 │
766
+ └────────┴──────────────┴──────────────┴───────────┴───────────────────────────┘
752
767
 
753
768
  Total: 3 studio(s)
754
769
  ```
755
770
 
771
+ **Formatting:**
772
+ - Full table borders with Unicode box-drawing characters
773
+ - User names are displayed in blue
774
+ - Status is color-coded: purple for "attached", green for "available", yellow for "attaching/detaching", red for "error"
775
+ - "Attached To" shows engine name, or "-" if not attached
776
+ - Studio IDs are displayed in grey
777
+ - User column width adjusts dynamically to fit the longest username
778
+ - Attached To column width adjusts dynamically to fit the longest engine name
779
+ - Columns are ordered: User, Status, Attached To, Size, Studio ID
780
+
756
781
  ---
757
782
 
758
783
  ### Attachment
@@ -587,34 +587,69 @@ def list_engines(env: Optional[str]):
587
587
  click.echo("No engines found")
588
588
  return
589
589
 
590
- # Table header - reordered per user request: Name, State, User, Type, Instance ID
590
+ # Calculate dynamic width for Name column (longest name + 2 for padding)
591
+ max_name_len = max((len(engine.get("name", "unknown")) for engine in engines), default=4)
592
+ name_width = max(max_name_len + 2, len("Name") + 2)
593
+
594
+ # Fixed widths for other columns
595
+ state_width = 12
596
+ user_width = 12
597
+ type_width = 12
598
+ id_width = 20
599
+
600
+ # Calculate total width for separator line
601
+ total_width = name_width + state_width + user_width + type_width + id_width + 9 # +9 for separators and spaces
602
+
603
+ # Table top border
604
+ click.echo("┌" + "─" * (name_width + 1) + "┬" + "─" * (state_width + 1) + "┬" + "─" * (user_width + 1) + "┬" + "─" * (type_width + 1) + "┬" + "─" * (id_width + 1) + "┐")
605
+
606
+ # Table header
591
607
  click.echo(
592
- f"{'Name':<30} {'State':<12} {'User':<12} {'Type':<12} {'Instance ID':<20}"
608
+ f"{'Name':<{name_width}}│ {'State':<{state_width}}│ {'User':<{user_width}}│ {'Type':<{type_width}}│ {'Instance ID':<{id_width}}│"
593
609
  )
594
- click.echo("-" * 90)
610
+
611
+ # Header separator
612
+ click.echo("├" + "─" * (name_width + 1) + "┼" + "─" * (state_width + 1) + "┼" + "─" * (user_width + 1) + "┼" + "─" * (type_width + 1) + "┼" + "─" * (id_width + 1) + "┤")
595
613
 
596
614
  # Table rows
597
615
  for engine in engines:
598
- name = engine.get("name", "unknown")[:29]
599
- state = engine.get("state", "unknown")[:11]
600
- user = engine.get("user", "unknown")[:11]
601
- engine_type = engine.get("engine_type", "unknown")[:11]
616
+ name = engine.get("name", "unknown")
617
+ state = engine.get("state", "unknown")
618
+ user = engine.get("user", "unknown")
619
+ engine_type = engine.get("engine_type", "unknown")
602
620
  instance_id = engine.get("instance_id", "unknown")
603
621
 
604
- # Color the state in the list view
622
+ # Truncate if needed
623
+ if len(name) > name_width - 1:
624
+ name = name[:name_width - 1]
625
+ if len(user) > user_width - 1:
626
+ user = user[:user_width - 1]
627
+ if len(engine_type) > type_width - 1:
628
+ engine_type = engine_type[:type_width - 1]
629
+
630
+ # Color the name (blue)
631
+ name_display = f"\033[34m{name:<{name_width}}\033[0m"
632
+
633
+ # Color the state
605
634
  if state == "running":
606
- state_display = f"\033[32m{state:<12}\033[0m" # Green
635
+ state_display = f"\033[32m{state:<{state_width}}\033[0m" # Green
607
636
  elif state in ["starting", "stopping", "pending"]:
608
- state_display = f"\033[33m{state:<12}\033[0m" # Yellow
637
+ state_display = f"\033[33m{state:<{state_width}}\033[0m" # Yellow
609
638
  elif state == "stopped":
610
- state_display = f"\033[37m{state:<12}\033[0m" # White
639
+ state_display = f"\033[90m{state:<{state_width}}\033[0m" # Grey (dim)
611
640
  else:
612
- state_display = f"{state:<12}" # No color for other states
641
+ state_display = f"{state:<{state_width}}" # No color for other states
642
+
643
+ # Color the instance ID (grey)
644
+ instance_id_display = f"\033[90m{instance_id:<{id_width}}\033[0m"
613
645
 
614
646
  click.echo(
615
- f"{name:<30} {state_display} {user:<12} {engine_type:<12} {instance_id:<20}"
647
+ f"{name_display} {state_display} {user:<{user_width}}│ {engine_type:<{type_width}}│ {instance_id_display}"
616
648
  )
617
649
 
650
+ # Table bottom border
651
+ click.echo("└" + "─" * (name_width + 1) + "┴" + "─" * (state_width + 1) + "┴" + "─" * (user_width + 1) + "┴" + "─" * (type_width + 1) + "┴" + "─" * (id_width + 1) + "┘")
652
+
618
653
  click.echo(f"\nTotal: {len(engines)} engine(s)")
619
654
 
620
655
  except Exception as e:
@@ -30,32 +30,104 @@ def format_list_output(engines: list[dict[str, Any]], env: str = "dev") -> None:
30
30
  print("No engines found")
31
31
  return
32
32
 
33
+ # Calculate dynamic width for Name column (longest name + 2 for padding)
34
+ max_name_len = max(
35
+ (len(engine.get("name", "unknown")) for engine in engines), default=4
36
+ )
37
+ name_width = max(max_name_len + 2, len("Name") + 2)
38
+
39
+ # Fixed widths for other columns
40
+ state_width = 12
41
+ user_width = 12
42
+ type_width = 12
43
+ id_width = 20
44
+
45
+ # Table top border
46
+ print(
47
+ "┌"
48
+ + "─" * (name_width + 1)
49
+ + "┬"
50
+ + "─" * (state_width + 1)
51
+ + "┬"
52
+ + "─" * (user_width + 1)
53
+ + "┬"
54
+ + "─" * (type_width + 1)
55
+ + "┬"
56
+ + "─" * (id_width + 1)
57
+ + "┐"
58
+ )
59
+
33
60
  # Table header
34
- print(f"{'Name':<12} {'State':<12} {'User':<12} {'Type':<12} {'Instance ID':<20}")
35
- print("-" * 72)
61
+ print(
62
+ f" {'Name':<{name_width}}│ {'State':<{state_width}}│ {'User':<{user_width}}│ {'Type':<{type_width}}│ {'Instance ID':<{id_width}}│"
63
+ )
64
+
65
+ # Header separator
66
+ print(
67
+ "├"
68
+ + "─" * (name_width + 1)
69
+ + "┼"
70
+ + "─" * (state_width + 1)
71
+ + "┼"
72
+ + "─" * (user_width + 1)
73
+ + "┼"
74
+ + "─" * (type_width + 1)
75
+ + "┼"
76
+ + "─" * (id_width + 1)
77
+ + "┤"
78
+ )
36
79
 
37
80
  # Table rows
38
81
  for engine in engines:
39
- name = engine.get("name", "unknown")[:11]
40
- state = engine.get("state", "unknown")[:11]
41
- user = engine.get("user", "unknown")[:11]
42
- engine_type = engine.get("engine_type", "unknown")[:11]
82
+ name = engine.get("name", "unknown")
83
+ state = engine.get("state", "unknown")
84
+ user = engine.get("user", "unknown")
85
+ engine_type = engine.get("engine_type", "unknown")
43
86
  instance_id = engine.get("instance_id", "unknown")
44
87
 
88
+ # Truncate if needed
89
+ if len(name) > name_width - 1:
90
+ name = name[: name_width - 1]
91
+ if len(user) > user_width - 1:
92
+ user = user[: user_width - 1]
93
+ if len(engine_type) > type_width - 1:
94
+ engine_type = engine_type[: type_width - 1]
95
+
96
+ # Color the name (blue)
97
+ name_display = colorize(f"{name:<{name_width}}", "34")
98
+
45
99
  # Color the state
46
100
  if state == "running":
47
- state_display = colorize(f"{state:<12}", "32") # Green
101
+ state_display = colorize(f"{state:<{state_width}}", "32") # Green
48
102
  elif state in ["starting", "stopping", "pending"]:
49
- state_display = colorize(f"{state:<12}", "33") # Yellow
103
+ state_display = colorize(f"{state:<{state_width}}", "33") # Yellow
50
104
  elif state == "stopped":
51
- state_display = colorize(f"{state:<12}", "37") # White
105
+ state_display = colorize(f"{state:<{state_width}}", "90") # Grey (dim)
52
106
  else:
53
- state_display = f"{state:<12}" # No color for other states
107
+ state_display = f"{state:<{state_width}}" # No color for other states
108
+
109
+ # Color the instance ID (grey)
110
+ instance_id_display = colorize(f"{instance_id:<{id_width}}", "90")
54
111
 
55
112
  print(
56
- f"{name:<12} {state_display} {user:<12} {engine_type:<12} {instance_id:<20}"
113
+ f"{name_display} {state_display} {user:<{user_width}}│ {engine_type:<{type_width}}│ {instance_id_display}"
57
114
  )
58
115
 
116
+ # Table bottom border
117
+ print(
118
+ "└"
119
+ + "─" * (name_width + 1)
120
+ + "┴"
121
+ + "─" * (state_width + 1)
122
+ + "┴"
123
+ + "─" * (user_width + 1)
124
+ + "┴"
125
+ + "─" * (type_width + 1)
126
+ + "┴"
127
+ + "─" * (id_width + 1)
128
+ + "┘"
129
+ )
130
+
59
131
  print(f"\nTotal: {len(engines)} engine(s)")
60
132
 
61
133
 
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env python3
2
+ """Simulator for studio list output - iterate on design locally without AWS.
3
+
4
+ This lets you quickly see how the list command output looks with different
5
+ studio states and configurations.
6
+
7
+ Usage:
8
+ python dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py # Show all scenarios
9
+ python dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py --scenario few # Show specific scenario
10
+ python dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py --env prod # Simulate different environment
11
+ """
12
+
13
+ import argparse
14
+ import sys
15
+ from typing import Any
16
+
17
+
18
+ def colorize(text: str, color_code: str) -> str:
19
+ """Apply ANSI color code to text."""
20
+ return f"\033[{color_code}m{text}\033[0m"
21
+
22
+
23
+ def format_list_output(studios: list[dict[str, Any]], engines_map: dict[str, str], env: str = "dev") -> None:
24
+ """Format and print studio list output matching the actual CLI."""
25
+
26
+ # Header with blue account name
27
+ print(f"Studios for AWS Account {colorize(env, '34')}\n")
28
+
29
+ if not studios:
30
+ print("No studios found")
31
+ return
32
+
33
+ # Calculate dynamic width for User column (longest user + 2 for padding)
34
+ max_user_len = max((len(studio.get("user", "unknown")) for studio in studios), default=4)
35
+ user_width = max(max_user_len + 2, len("User") + 2)
36
+
37
+ # Calculate dynamic width for Attached To column
38
+ max_attached_len = 0
39
+ for studio in studios:
40
+ if studio.get("attached_to"):
41
+ instance_id = studio["attached_to"]
42
+ engine_name = engines_map.get(instance_id, "unknown")
43
+ max_attached_len = max(max_attached_len, len(engine_name))
44
+ attached_width = max(max_attached_len + 2, len("Attached To") + 2, 3) # At least 3 for "-"
45
+
46
+ # Fixed widths for other columns
47
+ status_width = 12
48
+ size_width = 10
49
+ id_width = 25
50
+
51
+ # Table top border
52
+ print("┌" + "─" * (user_width + 1) + "┬" + "─" * (status_width + 1) + "┬" + "─" * (attached_width + 1) + "┬" + "─" * (size_width + 1) + "┬" + "─" * (id_width + 1) + "┐")
53
+
54
+ # Table header
55
+ print(f"│ {'User':<{user_width}}│ {'Status':<{status_width}}│ {'Attached To':<{attached_width}}│ {'Size':<{size_width}}│ {'Studio ID':<{id_width}}│")
56
+
57
+ # Header separator
58
+ print("├" + "─" * (user_width + 1) + "┼" + "─" * (status_width + 1) + "┼" + "─" * (attached_width + 1) + "┼" + "─" * (size_width + 1) + "┼" + "─" * (id_width + 1) + "┤")
59
+
60
+ # Table rows
61
+ for studio in studios:
62
+ user = studio.get("user", "unknown")
63
+ status = studio.get("status", "unknown")
64
+ size = f"{studio.get('size_gb', 0)}GB"
65
+ studio_id = studio.get("studio_id", "unknown")
66
+ attached_to = studio.get("attached_to")
67
+
68
+ # Truncate if needed
69
+ if len(user) > user_width - 1:
70
+ user = user[:user_width - 1]
71
+
72
+ # Color the user (blue)
73
+ user_display = colorize(f"{user:<{user_width}}", "34")
74
+
75
+ # Format status - display "in-use" as "attached" in purple
76
+ if status == "in-use":
77
+ display_status = "attached"
78
+ status_display = colorize(f"{display_status:<{status_width}}", "35") # Purple
79
+ elif status == "available":
80
+ status_display = colorize(f"{status:<{status_width}}", "32") # Green
81
+ elif status in ["attaching", "detaching"]:
82
+ status_display = colorize(f"{status:<{status_width}}", "33") # Yellow
83
+ elif status == "attached":
84
+ status_display = colorize(f"{status:<{status_width}}", "35") # Purple
85
+ elif status == "error":
86
+ status_display = colorize(f"{status:<{status_width}}", "31") # Red
87
+ else:
88
+ status_display = f"{status:<{status_width}}" # No color for other states
89
+
90
+ # Format Attached To column
91
+ if attached_to:
92
+ instance_id = attached_to
93
+ engine_name = engines_map.get(instance_id, "unknown")
94
+ # Engine name in white (no color)
95
+ attached_display = f"{engine_name:<{attached_width}}"
96
+ else:
97
+ attached_display = f"{'-':<{attached_width}}"
98
+
99
+ # Color the studio ID (grey)
100
+ studio_id_display = colorize(f"{studio_id:<{id_width}}", "90")
101
+
102
+ print(f"│ {user_display}│ {status_display}│ {attached_display}│ {size:<{size_width}}│ {studio_id_display}│")
103
+
104
+ # Table bottom border
105
+ print("└" + "─" * (user_width + 1) + "┴" + "─" * (status_width + 1) + "┴" + "─" * (attached_width + 1) + "┴" + "─" * (size_width + 1) + "┴" + "─" * (id_width + 1) + "┘")
106
+
107
+ print(f"\nTotal: {len(studios)} studio(s)")
108
+
109
+
110
+ def generate_scenarios() -> dict[str, dict[str, Any]]:
111
+ """Generate various test scenarios for studio list output."""
112
+
113
+ scenarios = {}
114
+
115
+ # Create a consistent engines map for all scenarios
116
+ engines_map = {
117
+ "i-0123456789abcdef0": "alice-gpu",
118
+ "i-1234567890abcdef1": "bob-cpu",
119
+ "i-2345678901abcdef2": "charlie-work",
120
+ "i-3456789012abcdef3": "diana-dev",
121
+ }
122
+
123
+ # Scenario 1: Single available studio
124
+ scenarios["single"] = {
125
+ "name": "Single Available Studio",
126
+ "studios": [
127
+ {
128
+ "user": "alice",
129
+ "status": "available",
130
+ "size_gb": 100,
131
+ "studio_id": "vol-0abc123def456789a",
132
+ "attached_to": None,
133
+ }
134
+ ],
135
+ "engines_map": engines_map,
136
+ "env": "dev",
137
+ }
138
+
139
+ # Scenario 2: Few studios with various states
140
+ scenarios["few"] = {
141
+ "name": "Few Studios - Mixed States",
142
+ "studios": [
143
+ {
144
+ "user": "alice",
145
+ "status": "in-use", # Will be displayed as "attached" in purple
146
+ "size_gb": 100,
147
+ "studio_id": "vol-0abc123def456789a",
148
+ "attached_to": "i-0123456789abcdef0",
149
+ },
150
+ {
151
+ "user": "bob",
152
+ "status": "available",
153
+ "size_gb": 200,
154
+ "studio_id": "vol-0abc123def456789b",
155
+ "attached_to": None,
156
+ },
157
+ {
158
+ "user": "charlie",
159
+ "status": "attaching",
160
+ "size_gb": 150,
161
+ "studio_id": "vol-0abc123def456789c",
162
+ "attached_to": "i-2345678901abcdef2",
163
+ },
164
+ ],
165
+ "engines_map": engines_map,
166
+ "env": "sand",
167
+ }
168
+
169
+ # Scenario 3: Many studios (production-like)
170
+ scenarios["many"] = {
171
+ "name": "Many Studios - Production",
172
+ "studios": [
173
+ {
174
+ "user": "alice",
175
+ "status": "attached",
176
+ "size_gb": 100,
177
+ "studio_id": "vol-0abc123def456789a",
178
+ "attached_to": "i-0123456789abcdef0",
179
+ },
180
+ {
181
+ "user": "bob",
182
+ "status": "attached",
183
+ "size_gb": 200,
184
+ "studio_id": "vol-0abc123def456789b",
185
+ "attached_to": "i-1234567890abcdef1",
186
+ },
187
+ {
188
+ "user": "charlie",
189
+ "status": "available",
190
+ "size_gb": 150,
191
+ "studio_id": "vol-0abc123def456789c",
192
+ "attached_to": None,
193
+ },
194
+ {
195
+ "user": "diana",
196
+ "status": "attached",
197
+ "size_gb": 250,
198
+ "studio_id": "vol-0abc123def456789d",
199
+ "attached_to": "i-3456789012abcdef3",
200
+ },
201
+ {
202
+ "user": "eve",
203
+ "status": "available",
204
+ "size_gb": 100,
205
+ "studio_id": "vol-0abc123def456789e",
206
+ "attached_to": None,
207
+ },
208
+ {
209
+ "user": "frank",
210
+ "status": "detaching",
211
+ "size_gb": 300,
212
+ "studio_id": "vol-0abc123def456789f",
213
+ "attached_to": None,
214
+ },
215
+ ],
216
+ "engines_map": engines_map,
217
+ "env": "prod",
218
+ }
219
+
220
+ # Scenario 4: Empty list
221
+ scenarios["empty"] = {
222
+ "name": "No Studios",
223
+ "studios": [],
224
+ "engines_map": engines_map,
225
+ "env": "dev",
226
+ }
227
+
228
+ # Scenario 5: All transitional states
229
+ scenarios["transitions"] = {
230
+ "name": "Transitional States",
231
+ "studios": [
232
+ {
233
+ "user": "alice",
234
+ "status": "attaching",
235
+ "size_gb": 100,
236
+ "studio_id": "vol-0abc123def456789a",
237
+ "attached_to": "i-0123456789abcdef0",
238
+ },
239
+ {
240
+ "user": "bob",
241
+ "status": "detaching",
242
+ "size_gb": 200,
243
+ "studio_id": "vol-0abc123def456789b",
244
+ "attached_to": None,
245
+ },
246
+ {
247
+ "user": "charlie",
248
+ "status": "error",
249
+ "size_gb": 150,
250
+ "studio_id": "vol-0abc123def456789c",
251
+ "attached_to": None,
252
+ },
253
+ ],
254
+ "engines_map": engines_map,
255
+ "env": "sand",
256
+ }
257
+
258
+ # Scenario 6: Long names
259
+ scenarios["long_names"] = {
260
+ "name": "Long User Names",
261
+ "studios": [
262
+ {
263
+ "user": "alice-with-very-long-username",
264
+ "status": "attached",
265
+ "size_gb": 100,
266
+ "studio_id": "vol-0abc123def456789a",
267
+ "attached_to": "i-0123456789abcdef0",
268
+ },
269
+ {
270
+ "user": "bob",
271
+ "status": "available",
272
+ "size_gb": 200,
273
+ "studio_id": "vol-0abc123def456789b",
274
+ "attached_to": None,
275
+ },
276
+ ],
277
+ "engines_map": engines_map,
278
+ "env": "dev",
279
+ }
280
+
281
+ return scenarios
282
+
283
+
284
+ def main():
285
+ parser = argparse.ArgumentParser(
286
+ description="Simulate studio list output for design iteration"
287
+ )
288
+ parser.add_argument(
289
+ "--scenario",
290
+ choices=["single", "few", "many", "empty", "transitions", "long_names", "all"],
291
+ default="all",
292
+ help="Which scenario to display (default: all)",
293
+ )
294
+ parser.add_argument(
295
+ "--env",
296
+ choices=["dev", "sand", "prod"],
297
+ help="Override environment for display",
298
+ )
299
+
300
+ args = parser.parse_args()
301
+
302
+ scenarios = generate_scenarios()
303
+
304
+ if args.scenario == "all":
305
+ # Show all scenarios
306
+ for _, scenario_data in scenarios.items():
307
+ print("\n" + "=" * 80)
308
+ print(f"SCENARIO: {scenario_data['name']}")
309
+ print("=" * 80 + "\n")
310
+
311
+ env = args.env if args.env else scenario_data["env"]
312
+ format_list_output(scenario_data["studios"], scenario_data["engines_map"], env)
313
+ print() # Extra newline between scenarios
314
+ else:
315
+ # Show specific scenario
316
+ scenario_data = scenarios[args.scenario]
317
+ print(f"\nSCENARIO: {scenario_data['name']}\n")
318
+
319
+ env = args.env if args.env else scenario_data["env"]
320
+ format_list_output(scenario_data["studios"], scenario_data["engines_map"], env)
321
+
322
+
323
+ if __name__ == "__main__":
324
+ main()
325
+
@@ -262,18 +262,89 @@ def list_studios(env: Optional[str]):
262
262
  click.echo("No studios found")
263
263
  return
264
264
 
265
- # Table header
266
- click.echo(f"{'User':<20} {'Studio ID':<25} {'Size':<10} {'Status':<15}")
267
- click.echo("-" * 75)
265
+ # Get all engines to map instance IDs to names
266
+ engines_result = client.list_engines()
267
+ engines_map = {}
268
+ for engine in engines_result.get("engines", []):
269
+ engines_map[engine["instance_id"]] = engine["name"]
270
+
271
+ # Calculate dynamic width for User column (longest user + 2 for padding)
272
+ max_user_len = max((len(studio.get("user", "unknown")) for studio in studios), default=4)
273
+ user_width = max(max_user_len + 2, len("User") + 2)
274
+
275
+ # Calculate dynamic width for Attached To column
276
+ max_attached_len = 0
277
+ for studio in studios:
278
+ if studio.get("attached_to"):
279
+ instance_id = studio["attached_to"]
280
+ engine_name = engines_map.get(instance_id, "unknown")
281
+ max_attached_len = max(max_attached_len, len(engine_name))
282
+ attached_width = max(max_attached_len + 2, len("Attached To") + 2, 3) # At least 3 for "-"
283
+
284
+ # Fixed widths for other columns - reordered to [User, Status, Attached To, Size, Studio ID]
285
+ status_width = 12
286
+ size_width = 10
287
+ id_width = 25
288
+
289
+ # Table top border
290
+ click.echo("┌" + "─" * (user_width + 1) + "┬" + "─" * (status_width + 1) + "┬" + "─" * (attached_width + 1) + "┬" + "─" * (size_width + 1) + "┬" + "─" * (id_width + 1) + "┐")
291
+
292
+ # Table header - reordered to [User, Status, Attached To, Size, Studio ID]
293
+ click.echo(
294
+ f"│ {'User':<{user_width}}│ {'Status':<{status_width}}│ {'Attached To':<{attached_width}}│ {'Size':<{size_width}}│ {'Studio ID':<{id_width}}│"
295
+ )
296
+
297
+ # Header separator
298
+ click.echo("├" + "─" * (user_width + 1) + "┼" + "─" * (status_width + 1) + "┼" + "─" * (attached_width + 1) + "┼" + "─" * (size_width + 1) + "┼" + "─" * (id_width + 1) + "┤")
268
299
 
269
300
  # Table rows
270
301
  for studio in studios:
271
- user = studio.get("user", "unknown")[:19]
272
- studio_id = studio.get("studio_id", "unknown")
273
- size = f"{studio.get('size_gb', 0)}GB"
302
+ user = studio.get("user", "unknown")
274
303
  status = studio.get("status", "unknown")
304
+ size = f"{studio.get('size_gb', 0)}GB"
305
+ studio_id = studio.get("studio_id", "unknown")
306
+ attached_to = studio.get("attached_to")
307
+
308
+ # Truncate if needed
309
+ if len(user) > user_width - 1:
310
+ user = user[:user_width - 1]
311
+
312
+ # Color the user (blue)
313
+ user_display = f"\033[34m{user:<{user_width}}\033[0m"
314
+
315
+ # Format status - display "in-use" as "attached" in purple
316
+ if status == "in-use":
317
+ display_status = "attached"
318
+ status_display = f"\033[35m{display_status:<{status_width}}\033[0m" # Purple
319
+ elif status == "available":
320
+ status_display = f"\033[32m{status:<{status_width}}\033[0m" # Green
321
+ elif status in ["attaching", "detaching"]:
322
+ status_display = f"\033[33m{status:<{status_width}}\033[0m" # Yellow
323
+ elif status == "attached":
324
+ status_display = f"\033[35m{status:<{status_width}}\033[0m" # Purple
325
+ elif status == "error":
326
+ status_display = f"\033[31m{status:<{status_width}}\033[0m" # Red for error
327
+ else:
328
+ status_display = f"{status:<{status_width}}" # No color for other states
329
+
330
+ # Format Attached To column
331
+ if attached_to:
332
+ instance_id = attached_to
333
+ engine_name = engines_map.get(instance_id, "unknown")
334
+ # Engine name in white (no color)
335
+ attached_display = f"{engine_name:<{attached_width}}"
336
+ else:
337
+ attached_display = f"{'-':<{attached_width}}"
338
+
339
+ # Color the studio ID (grey)
340
+ studio_id_display = f"\033[90m{studio_id:<{id_width}}\033[0m"
341
+
342
+ click.echo(
343
+ f"│ {user_display}│ {status_display}│ {attached_display}│ {size:<{size_width}}│ {studio_id_display}│"
344
+ )
275
345
 
276
- click.echo(f"{user:<20} {studio_id:<25} {size:<10} {status:<15}")
346
+ # Table bottom border
347
+ click.echo("└" + "─" * (user_width + 1) + "┴" + "─" * (status_width + 1) + "┴" + "─" * (attached_width + 1) + "┴" + "─" * (size_width + 1) + "┴" + "─" * (id_width + 1) + "┘")
277
348
 
278
349
  click.echo(f"\nTotal: {len(studios)} studio(s)")
279
350
 
@@ -344,6 +344,47 @@ def create_or_update_job_definition(
344
344
  f"Adding mount points to job definition: {batch_job_config['mountPoints']}"
345
345
  )
346
346
 
347
+ # Auto-mount Primordial Drive if available
348
+ # This ensures all batch jobs have access to shared datasets at /primordial/
349
+ primordial_fs_id = get_primordial_fs_id(session)
350
+ if primordial_fs_id:
351
+ print(f"Adding Primordial Drive configuration (fs_id: {primordial_fs_id})")
352
+
353
+ # Add volume configuration
354
+ efs_volume = {
355
+ "name": "primordial",
356
+ "efsVolumeConfiguration": {
357
+ "fileSystemId": primordial_fs_id,
358
+ "rootDirectory": "/",
359
+ },
360
+ }
361
+
362
+ if "volumes" not in container_properties:
363
+ container_properties["volumes"] = []
364
+
365
+ # Check if already added to avoid duplicates
366
+ if not any(
367
+ v.get("name") == "primordial" for v in container_properties["volumes"]
368
+ ):
369
+ container_properties["volumes"].append(efs_volume)
370
+
371
+ # Add mount point
372
+ mount_point = {
373
+ "sourceVolume": "primordial",
374
+ "containerPath": "/primordial",
375
+ "readOnly": False,
376
+ }
377
+
378
+ if "mountPoints" not in container_properties:
379
+ container_properties["mountPoints"] = []
380
+
381
+ # Check if already added
382
+ if not any(
383
+ mp.get("containerPath") == "/primordial"
384
+ for mp in container_properties["mountPoints"]
385
+ ):
386
+ container_properties["mountPoints"].append(mount_point)
387
+
347
388
  # Check if job definition already exists using the session client
348
389
  try:
349
390
  existing = batch.describe_job_definitions(
@@ -385,6 +426,38 @@ def create_or_update_job_definition(
385
426
  return response["jobDefinitionName"]
386
427
 
387
428
 
429
+ def get_primordial_fs_id(session: boto3.Session) -> Optional[str]:
430
+ """Fetch Primordial Drive EFS ID from SSM.
431
+
432
+ Args:
433
+ session: Boto3 session
434
+
435
+ Returns:
436
+ FileSystemId if found, None otherwise
437
+ """
438
+ ssm = session.client("ssm")
439
+
440
+ # Determine environment from profile name
441
+ # Default to dev if cannot determine
442
+ env = "dev"
443
+ if session.profile_name and "sand" in session.profile_name:
444
+ env = "sand"
445
+
446
+ param_name = f"/{env}/primordial/fs_id"
447
+
448
+ try:
449
+ response = ssm.get_parameter(Name=param_name)
450
+ return response["Parameter"]["Value"]
451
+ except ClientError as e:
452
+ # Silently fail if not found - Primordial might not be deployed in this env
453
+ # or we might not have permissions
454
+ # ParameterNotFound is a ClientError with error code "ParameterNotFound"
455
+ return None
456
+ except Exception as e:
457
+ print(f"Warning: Failed to check for Primordial Drive: {e}")
458
+ return None
459
+
460
+
388
461
  def submit_aws_batch_job(
389
462
  image_uri: str,
390
463
  config: dict[str, Any],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dayhoff-tools
3
- Version: 1.12.31
3
+ Version: 1.12.33
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
@@ -13,22 +13,23 @@ dayhoff_tools/cli/engine1/studio_commands.py,sha256=VwTQujz32-uMcYusDRE73SdzRpgv
13
13
  dayhoff_tools/cli/engines_studios/__init__.py,sha256=E6aG0C6qjJnJuClemSKRFlYvLUL49MQZOvfqNQ7SDKs,159
14
14
  dayhoff_tools/cli/engines_studios/api_client.py,sha256=9I55_Ns8VHxndGjvSt_c5ZohSqMOeywQlLjyuoDEqCQ,13039
15
15
  dayhoff_tools/cli/engines_studios/auth.py,sha256=rwetV5hp4jSvK8FyvKgXCnezLOZx1aW8oiSDc6U83iE,5189
16
- dayhoff_tools/cli/engines_studios/engine-studio-cli.md,sha256=jwUoMrwp8OnNcOMw128NnQib9Yn2tWmupXcLLtyfass,27613
17
- dayhoff_tools/cli/engines_studios/engine_commands.py,sha256=x7fif4bnc0A8sDNHv3UuilUSurWv8Qhfrv9Jlp1s_QA,32750
16
+ dayhoff_tools/cli/engines_studios/engine-studio-cli.md,sha256=TYnoJUxuTsnsvUAbn0oxlp0nCru9eYElL1_LLCvtqw4,29944
17
+ dayhoff_tools/cli/engines_studios/engine_commands.py,sha256=Kmt9lo5bEmLDbIqNX6ivqWIgoTH9XrbOsXmHpqeVaqs,34561
18
18
  dayhoff_tools/cli/engines_studios/progress.py,sha256=SMahdG2YmO5bEPSONrfAXVTdS6m_69Ep02t3hc2DdKQ,9264
19
19
  dayhoff_tools/cli/engines_studios/simulators/cli-simulators.md,sha256=FZJl6nehdr2Duht2cx3yijcak0yKyOaHTrTzvFTAfZs,4976
20
20
  dayhoff_tools/cli/engines_studios/simulators/demo.sh,sha256=8tYABSCxLNXqGs-4r071V9mpKNZ5DTQ34WZ-v3d5s94,5364
21
- dayhoff_tools/cli/engines_studios/simulators/engine_list_simulator.py,sha256=MrfcCpg8oKZ3fpr9-J8rb7pdJ7XptKuhLcVSF07xxxU,7601
21
+ dayhoff_tools/cli/engines_studios/simulators/engine_list_simulator.py,sha256=ldPA6D4EqnCn6zjcbDX2AWRtVZ3SXCyUTfFbVStabvc,9533
22
22
  dayhoff_tools/cli/engines_studios/simulators/engine_status_simulator.py,sha256=KUm3gA2MiRgGrQV7KURhb5zabM18-30z_ugRjiq5iso,13024
23
23
  dayhoff_tools/cli/engines_studios/simulators/idle_status_simulator.py,sha256=F_MfEXdPKNVDCKgJV72QyU2oMG8hLt-Bwic4yFadRXE,17570
24
24
  dayhoff_tools/cli/engines_studios/simulators/simulator_utils.py,sha256=HA08pIMJWV3OFrWj3Ca8GldvgJZfFoTOloyLK0UWMgA,6729
25
+ dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py,sha256=kOoON4_V5DwPRBlCuFqOEEjH535a_SyDAFoK5wKlYaY,11205
25
26
  dayhoff_tools/cli/engines_studios/simulators/studio_status_simulator.py,sha256=6WvpnRawJVaQf_H81zuR1_66igRRVxPxjAt8e69xjp4,5394
26
- dayhoff_tools/cli/engines_studios/studio_commands.py,sha256=fXj0Oo8BTWrORplg3V0dxZo1i9xX6-h2WSsdtbJQqbc,20575
27
+ dayhoff_tools/cli/engines_studios/studio_commands.py,sha256=V7yltLuwN67xGOCj6S8DyfyQDQtjJbANq7G6gY5P08s,24275
27
28
  dayhoff_tools/cli/main.py,sha256=Nz_jtbppmvWKHZydQ0nkt_eejccJE90ces8xCGrerdY,7086
28
29
  dayhoff_tools/cli/swarm_commands.py,sha256=5EyKj8yietvT5lfoz8Zx0iQvVaNgc3SJX1z2zQR6o6M,5614
29
30
  dayhoff_tools/cli/utility_commands.py,sha256=e2P4dCCtoqMUGNyb0lFBZ6GZpl5Zslm1qqE5qIvsy38,50765
30
31
  dayhoff_tools/deployment/base.py,sha256=fM9zyhuRvIK8YqY6ooYg9j6wy_8touA_L-dkV7FA5q4,18058
31
- dayhoff_tools/deployment/deploy_aws.py,sha256=gfqh09hGbz0q3oPqVm0imd_CEjKF2k8moGNRIL26qqE,18614
32
+ dayhoff_tools/deployment/deploy_aws.py,sha256=3xNuGvwDKvICfiLXVeM4Oz8MXQ363voTM-6vXHtEHZY,20987
32
33
  dayhoff_tools/deployment/deploy_gcp.py,sha256=xgaOVsUDmP6wSEMYNkm1yRNcVskfdz80qJtCulkBIAM,8860
33
34
  dayhoff_tools/deployment/deploy_utils.py,sha256=KyUFZZWn8NGT9QpR0HGqkX-huOFubvYCabko9SlC5Gg,26516
34
35
  dayhoff_tools/deployment/job_runner.py,sha256=hljvFpH2Bw96uYyUup5Ths72PZRL_X27KxlYzBMgguo,5086
@@ -47,7 +48,7 @@ dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJq
47
48
  dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
48
49
  dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
49
50
  dayhoff_tools/warehouse.py,sha256=UETBtZD3r7WgvURqfGbyHlT7cxoiVq8isjzMuerKw8I,24475
50
- dayhoff_tools-1.12.31.dist-info/METADATA,sha256=XXc2Zu9LH0HKmDqY-uj2MlRNYrm4sOtxIHVdzRozBgg,2981
51
- dayhoff_tools-1.12.31.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
52
- dayhoff_tools-1.12.31.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
53
- dayhoff_tools-1.12.31.dist-info/RECORD,,
51
+ dayhoff_tools-1.12.33.dist-info/METADATA,sha256=eitG3L1hL3vbXaglvoQOs25y5arWRzklHRntUTBVUR0,2981
52
+ dayhoff_tools-1.12.33.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
53
+ dayhoff_tools-1.12.33.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
54
+ dayhoff_tools-1.12.33.dist-info/RECORD,,