dayhoff-tools 1.12.32__py3-none-any.whl → 1.13.0__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.
@@ -323,18 +323,22 @@ dh engine2 list --env sand
323
323
  ```
324
324
  Engines for AWS Account dev
325
325
 
326
- Name State User Type Instance ID
327
- ───────────────────────────────────────────────────────────────────────────
328
- alice-work running alice cpu i-0123456789abcdef0
329
- bob-training running bob a10g i-0fedcba987654321
330
- batch-worker stopped charlie cpumax i-0abc123def456789
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
+ └──────────────┴─────────────┴─────────────┴─────────────┴─────────────────────┘
331
333
 
332
334
  Total: 3 engine(s)
333
335
  ```
334
336
 
335
337
  **Formatting:**
338
+ - Full table borders with Unicode box-drawing characters
336
339
  - Engine names are displayed in blue
337
340
  - State is color-coded: green for "running", yellow for "starting/stopping", grey for "stopped"
341
+ - Instance IDs are displayed in grey
338
342
  - Name column width adjusts dynamically to fit the longest engine name
339
343
 
340
344
  ---
@@ -751,20 +755,28 @@ dh studio2 list
751
755
 
752
756
  **Output:**
753
757
  ```
754
- User Status Size Studio ID
755
- ──────────────────────────────────────────────────────────────
756
- alice attached 100GB vol-0123456789abcdef0
757
- bob available 200GB vol-0fedcba987654321
758
- carol available 150GB vol-0abc123def456789
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
+ └────────┴──────────────┴──────────────┴───────────┴───────────────────────────┘
759
767
 
760
768
  Total: 3 studio(s)
761
769
  ```
762
770
 
763
771
  **Formatting:**
772
+ - Full table borders with Unicode box-drawing characters
764
773
  - User names are displayed in blue
765
- - Status is color-coded: green for "available/attached", yellow for "attaching/detaching", red for "error"
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
766
777
  - User column width adjusts dynamically to fit the longest username
767
- - Columns are ordered: User, Status, Size, Studio ID
778
+ - Attached To column width adjusts dynamically to fit the longest engine name
779
+ - Columns are ordered: User, Status, Attached To, Size, Studio ID
768
780
 
769
781
  ---
770
782
 
@@ -598,13 +598,18 @@ def list_engines(env: Optional[str]):
598
598
  id_width = 20
599
599
 
600
600
  # Calculate total width for separator line
601
- total_width = name_width + state_width + user_width + type_width + id_width + 4 # +4 for spaces
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) + "┐")
602
605
 
603
606
  # Table header
604
607
  click.echo(
605
- f"{'Name':<{name_width}} {'State':<{state_width}} {'User':<{user_width}} {'Type':<{type_width}} {'Instance ID':<{id_width}}"
608
+ f"{'Name':<{name_width}} {'State':<{state_width}} {'User':<{user_width}} {'Type':<{type_width}} {'Instance ID':<{id_width}}"
606
609
  )
607
- click.echo("─" * total_width)
610
+
611
+ # Header separator
612
+ click.echo("├" + "─" * (name_width + 1) + "┼" + "─" * (state_width + 1) + "┼" + "─" * (user_width + 1) + "┼" + "─" * (type_width + 1) + "┼" + "─" * (id_width + 1) + "┤")
608
613
 
609
614
  # Table rows
610
615
  for engine in engines:
@@ -635,10 +640,16 @@ def list_engines(env: Optional[str]):
635
640
  else:
636
641
  state_display = f"{state:<{state_width}}" # No color for other states
637
642
 
643
+ # Color the instance ID (grey)
644
+ instance_id_display = f"\033[90m{instance_id:<{id_width}}\033[0m"
645
+
638
646
  click.echo(
639
- f"{name_display} {state_display} {user:<{user_width}} {engine_type:<{type_width}} {instance_id:<{id_width}}"
647
+ f"{name_display} {state_display} {user:<{user_width}} {engine_type:<{type_width}} {instance_id_display}"
640
648
  )
641
649
 
650
+ # Table bottom border
651
+ click.echo("└" + "─" * (name_width + 1) + "┴" + "─" * (state_width + 1) + "┴" + "─" * (user_width + 1) + "┴" + "─" * (type_width + 1) + "┴" + "─" * (id_width + 1) + "┘")
652
+
642
653
  click.echo(f"\nTotal: {len(engines)} engine(s)")
643
654
 
644
655
  except Exception as e:
@@ -31,7 +31,9 @@ def format_list_output(engines: list[dict[str, Any]], env: str = "dev") -> None:
31
31
  return
32
32
 
33
33
  # Calculate dynamic width for Name column (longest name + 2 for padding)
34
- max_name_len = max((len(engine.get("name", "unknown")) for engine in engines), default=4)
34
+ max_name_len = max(
35
+ (len(engine.get("name", "unknown")) for engine in engines), default=4
36
+ )
35
37
  name_width = max(max_name_len + 2, len("Name") + 2)
36
38
 
37
39
  # Fixed widths for other columns
@@ -40,12 +42,40 @@ def format_list_output(engines: list[dict[str, Any]], env: str = "dev") -> None:
40
42
  type_width = 12
41
43
  id_width = 20
42
44
 
43
- # Calculate total width for separator line
44
- total_width = name_width + state_width + user_width + type_width + id_width + 4 # +4 for spaces
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
+ )
45
59
 
46
60
  # Table header
47
- print(f"{'Name':<{name_width}} {'State':<{state_width}} {'User':<{user_width}} {'Type':<{type_width}} {'Instance ID':<{id_width}}")
48
- print("─" * total_width)
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
+ )
49
79
 
50
80
  # Table rows
51
81
  for engine in engines:
@@ -57,11 +87,11 @@ def format_list_output(engines: list[dict[str, Any]], env: str = "dev") -> None:
57
87
 
58
88
  # Truncate if needed
59
89
  if len(name) > name_width - 1:
60
- name = name[:name_width - 1]
90
+ name = name[: name_width - 1]
61
91
  if len(user) > user_width - 1:
62
- user = user[:user_width - 1]
92
+ user = user[: user_width - 1]
63
93
  if len(engine_type) > type_width - 1:
64
- engine_type = engine_type[:type_width - 1]
94
+ engine_type = engine_type[: type_width - 1]
65
95
 
66
96
  # Color the name (blue)
67
97
  name_display = colorize(f"{name:<{name_width}}", "34")
@@ -76,10 +106,28 @@ def format_list_output(engines: list[dict[str, Any]], env: str = "dev") -> None:
76
106
  else:
77
107
  state_display = f"{state:<{state_width}}" # No color for other states
78
108
 
109
+ # Color the instance ID (grey)
110
+ instance_id_display = colorize(f"{instance_id:<{id_width}}", "90")
111
+
79
112
  print(
80
- f"{name_display} {state_display} {user:<{user_width}} {engine_type:<{type_width}} {instance_id:<{id_width}}"
113
+ f"{name_display} {state_display} {user:<{user_width}} {engine_type:<{type_width}} {instance_id_display}"
81
114
  )
82
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
+
83
131
  print(f"\nTotal: {len(engines)} engine(s)")
84
132
 
85
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,23 +262,40 @@ def list_studios(env: Optional[str]):
262
262
  click.echo("No studios found")
263
263
  return
264
264
 
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
+
265
271
  # Calculate dynamic width for User column (longest user + 2 for padding)
266
272
  max_user_len = max((len(studio.get("user", "unknown")) for studio in studios), default=4)
267
273
  user_width = max(max_user_len + 2, len("User") + 2)
268
274
 
269
- # Fixed widths for other columns - reordered to [User, Status, Size, Studio ID]
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]
270
285
  status_width = 12
271
286
  size_width = 10
272
287
  id_width = 25
273
288
 
274
- # Calculate total width for separator line
275
- total_width = user_width + status_width + size_width + id_width + 3 # +3 for spaces
289
+ # Table top border
290
+ click.echo("┌" + "─" * (user_width + 1) + "┬" + "─" * (status_width + 1) + "┬" + "─" * (attached_width + 1) + "┬" + "─" * (size_width + 1) + "┬" + "─" * (id_width + 1) + "┐")
276
291
 
277
- # Table header - reordered to [User, Status, Size, Studio ID]
292
+ # Table header - reordered to [User, Status, Attached To, Size, Studio ID]
278
293
  click.echo(
279
- f"{'User':<{user_width}} {'Status':<{status_width}} {'Size':<{size_width}} {'Studio ID':<{id_width}}"
294
+ f"{'User':<{user_width}} {'Status':<{status_width}} {'Attached To':<{attached_width}}│ {'Size':<{size_width}} {'Studio ID':<{id_width}}"
280
295
  )
281
- click.echo("─" * total_width)
296
+
297
+ # Header separator
298
+ click.echo("├" + "─" * (user_width + 1) + "┼" + "─" * (status_width + 1) + "┼" + "─" * (attached_width + 1) + "┼" + "─" * (size_width + 1) + "┼" + "─" * (id_width + 1) + "┤")
282
299
 
283
300
  # Table rows
284
301
  for studio in studios:
@@ -286,6 +303,7 @@ def list_studios(env: Optional[str]):
286
303
  status = studio.get("status", "unknown")
287
304
  size = f"{studio.get('size_gb', 0)}GB"
288
305
  studio_id = studio.get("studio_id", "unknown")
306
+ attached_to = studio.get("attached_to")
289
307
 
290
308
  # Truncate if needed
291
309
  if len(user) > user_width - 1:
@@ -294,26 +312,40 @@ def list_studios(env: Optional[str]):
294
312
  # Color the user (blue)
295
313
  user_display = f"\033[34m{user:<{user_width}}\033[0m"
296
314
 
297
- # Color the status
298
- if status == "available":
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":
299
320
  status_display = f"\033[32m{status:<{status_width}}\033[0m" # Green
300
321
  elif status in ["attaching", "detaching"]:
301
322
  status_display = f"\033[33m{status:<{status_width}}\033[0m" # Yellow
302
323
  elif status == "attached":
303
- status_display = f"\033[32m{status:<{status_width}}\033[0m" # Green
304
- elif status in ["error", "in-use"]:
305
- # "in-use" is the old status name, treat as attached (green)
306
- if status == "in-use":
307
- status_display = f"\033[32m{status:<{status_width}}\033[0m" # Green
308
- else:
309
- status_display = f"\033[31m{status:<{status_width}}\033[0m" # Red for error
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
310
327
  else:
311
328
  status_display = f"{status:<{status_width}}" # No color for other states
312
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
+
313
342
  click.echo(
314
- f"{user_display} {status_display} {size:<{size_width}} {studio_id:<{id_width}}"
343
+ f"{user_display} {status_display} {attached_display}│ {size:<{size_width}} {studio_id_display}"
315
344
  )
316
345
 
346
+ # Table bottom border
347
+ click.echo("└" + "─" * (user_width + 1) + "┴" + "─" * (status_width + 1) + "┴" + "─" * (attached_width + 1) + "┴" + "─" * (size_width + 1) + "┴" + "─" * (id_width + 1) + "┘")
348
+
317
349
  click.echo(f"\nTotal: {len(studios)} studio(s)")
318
350
 
319
351
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dayhoff-tools
3
- Version: 1.12.32
3
+ Version: 1.13.0
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,17 +13,18 @@ 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=uWi6MMZwlIc1PiNQ8iRtRZSVTSOmBkwWY1s2wpuRbgk,28345
17
- dayhoff_tools/cli/engines_studios/engine_commands.py,sha256=zhkKsXj6LzpfJ2MnNsmir-pByKOuGSXLeJU14pVJfaQ,33743
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=hkuZMkCfnAD8giMVS04TeqGWMiRKhjhCmKCPRWcdPZA,8603
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=DX--3p15yrKC5eB86dc5m_MTb-5SB7b-kPsqW42ykaI,22417
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
@@ -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.32.dist-info/METADATA,sha256=wg00QqtusBsm7HGPud5mkObekKEuyJoa-IYpRRquZ5k,2981
51
- dayhoff_tools-1.12.32.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
52
- dayhoff_tools-1.12.32.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
53
- dayhoff_tools-1.12.32.dist-info/RECORD,,
51
+ dayhoff_tools-1.13.0.dist-info/METADATA,sha256=J7tfJUuPi4-ncz_TgTx4tZqbkpEey8KxwdSM8M3mvCg,2980
52
+ dayhoff_tools-1.13.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
53
+ dayhoff_tools-1.13.0.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
54
+ dayhoff_tools-1.13.0.dist-info/RECORD,,