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.
- dayhoff_tools/cli/engines_studios/engine-studio-cli.md +35 -10
- dayhoff_tools/cli/engines_studios/engine_commands.py +48 -13
- dayhoff_tools/cli/engines_studios/simulators/engine_list_simulator.py +83 -11
- dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py +325 -0
- dayhoff_tools/cli/engines_studios/studio_commands.py +78 -7
- dayhoff_tools/deployment/deploy_aws.py +73 -0
- {dayhoff_tools-1.12.31.dist-info → dayhoff_tools-1.12.33.dist-info}/METADATA +1 -1
- {dayhoff_tools-1.12.31.dist-info → dayhoff_tools-1.12.33.dist-info}/RECORD +10 -9
- {dayhoff_tools-1.12.31.dist-info → dayhoff_tools-1.12.33.dist-info}/WHEEL +0 -0
- {dayhoff_tools-1.12.31.dist-info → dayhoff_tools-1.12.33.dist-info}/entry_points.txt +0 -0
|
@@ -321,15 +321,26 @@ dh engine2 list --env sand
|
|
|
321
321
|
|
|
322
322
|
**Output:**
|
|
323
323
|
```
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
#
|
|
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':<
|
|
608
|
+
f"│ {'Name':<{name_width}}│ {'State':<{state_width}}│ {'User':<{user_width}}│ {'Type':<{type_width}}│ {'Instance ID':<{id_width}}│"
|
|
593
609
|
)
|
|
594
|
-
|
|
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")
|
|
599
|
-
state = engine.get("state", "unknown")
|
|
600
|
-
user = engine.get("user", "unknown")
|
|
601
|
-
engine_type = engine.get("engine_type", "unknown")
|
|
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
|
-
#
|
|
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:<
|
|
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:<
|
|
637
|
+
state_display = f"\033[33m{state:<{state_width}}\033[0m" # Yellow
|
|
609
638
|
elif state == "stopped":
|
|
610
|
-
state_display = f"\033[
|
|
639
|
+
state_display = f"\033[90m{state:<{state_width}}\033[0m" # Grey (dim)
|
|
611
640
|
else:
|
|
612
|
-
state_display = f"{state:<
|
|
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"{
|
|
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(
|
|
35
|
-
|
|
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")
|
|
40
|
-
state = engine.get("state", "unknown")
|
|
41
|
-
user = engine.get("user", "unknown")
|
|
42
|
-
engine_type = engine.get("engine_type", "unknown")
|
|
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:<
|
|
101
|
+
state_display = colorize(f"{state:<{state_width}}", "32") # Green
|
|
48
102
|
elif state in ["starting", "stopping", "pending"]:
|
|
49
|
-
state_display = colorize(f"{state:<
|
|
103
|
+
state_display = colorize(f"{state:<{state_width}}", "33") # Yellow
|
|
50
104
|
elif state == "stopped":
|
|
51
|
-
state_display = colorize(f"{state:<
|
|
105
|
+
state_display = colorize(f"{state:<{state_width}}", "90") # Grey (dim)
|
|
52
106
|
else:
|
|
53
|
-
state_display = f"{state:<
|
|
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"{
|
|
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
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
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")
|
|
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
|
-
|
|
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],
|
|
@@ -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=
|
|
17
|
-
dayhoff_tools/cli/engines_studios/engine_commands.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
51
|
-
dayhoff_tools-1.12.
|
|
52
|
-
dayhoff_tools-1.12.
|
|
53
|
-
dayhoff_tools-1.12.
|
|
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,,
|
|
File without changes
|
|
File without changes
|