dayhoff-tools 1.1.10__py3-none-any.whl → 1.13.12__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/__init__.py +10 -0
- dayhoff_tools/cli/cloud_commands.py +179 -43
- dayhoff_tools/cli/engine1/__init__.py +323 -0
- dayhoff_tools/cli/engine1/engine_core.py +703 -0
- dayhoff_tools/cli/engine1/engine_lifecycle.py +136 -0
- dayhoff_tools/cli/engine1/engine_maintenance.py +431 -0
- dayhoff_tools/cli/engine1/engine_management.py +505 -0
- dayhoff_tools/cli/engine1/shared.py +501 -0
- dayhoff_tools/cli/engine1/studio_commands.py +825 -0
- dayhoff_tools/cli/engines_studios/__init__.py +6 -0
- dayhoff_tools/cli/engines_studios/api_client.py +351 -0
- dayhoff_tools/cli/engines_studios/auth.py +144 -0
- dayhoff_tools/cli/engines_studios/engine-studio-cli.md +1230 -0
- dayhoff_tools/cli/engines_studios/engine_commands.py +1151 -0
- dayhoff_tools/cli/engines_studios/progress.py +260 -0
- dayhoff_tools/cli/engines_studios/simulators/cli-simulators.md +151 -0
- dayhoff_tools/cli/engines_studios/simulators/demo.sh +75 -0
- dayhoff_tools/cli/engines_studios/simulators/engine_list_simulator.py +319 -0
- dayhoff_tools/cli/engines_studios/simulators/engine_status_simulator.py +369 -0
- dayhoff_tools/cli/engines_studios/simulators/idle_status_simulator.py +476 -0
- dayhoff_tools/cli/engines_studios/simulators/simulator_utils.py +180 -0
- dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py +374 -0
- dayhoff_tools/cli/engines_studios/simulators/studio_status_simulator.py +164 -0
- dayhoff_tools/cli/engines_studios/studio_commands.py +755 -0
- dayhoff_tools/cli/main.py +106 -7
- dayhoff_tools/cli/utility_commands.py +896 -179
- dayhoff_tools/deployment/base.py +70 -6
- dayhoff_tools/deployment/deploy_aws.py +165 -25
- dayhoff_tools/deployment/deploy_gcp.py +78 -5
- dayhoff_tools/deployment/deploy_utils.py +20 -7
- dayhoff_tools/deployment/job_runner.py +9 -4
- dayhoff_tools/deployment/processors.py +230 -418
- dayhoff_tools/deployment/swarm.py +47 -12
- dayhoff_tools/embedders.py +28 -26
- dayhoff_tools/fasta.py +181 -64
- dayhoff_tools/warehouse.py +268 -1
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/METADATA +20 -5
- dayhoff_tools-1.13.12.dist-info/RECORD +54 -0
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/WHEEL +1 -1
- dayhoff_tools-1.1.10.dist-info/RECORD +0 -32
- {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Simulator for engine status output - iterate on design locally without AWS.
|
|
3
|
+
|
|
4
|
+
This lets you quickly see how the status command output looks under different
|
|
5
|
+
engine states and sensor combinations.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python dayhoff_tools/cli/engines_studios/idle_status_simulator.py # Show all scenarios
|
|
9
|
+
python dayhoff_tools/cli/engines_studios/idle_status_simulator.py --scenario idle # Show specific scenario
|
|
10
|
+
python dayhoff_tools/cli/engines_studios/idle_status_simulator.py --colorful # Use more emojis
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime, timedelta, timezone
|
|
16
|
+
|
|
17
|
+
# Import standalone utilities
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
simulator_dir = Path(__file__).parent
|
|
21
|
+
sys.path.insert(0, str(simulator_dir))
|
|
22
|
+
from simulator_utils import format_idle_state, format_time_ago
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_scenarios():
|
|
26
|
+
"""Generate various test scenarios for status output."""
|
|
27
|
+
|
|
28
|
+
scenarios = {}
|
|
29
|
+
|
|
30
|
+
# Scenario 1: Completely idle engine
|
|
31
|
+
scenarios["idle"] = {
|
|
32
|
+
"name": "Completely Idle Engine",
|
|
33
|
+
"status_data": {
|
|
34
|
+
"name": "alice-work",
|
|
35
|
+
"instance_id": "i-0123456789abcdef0",
|
|
36
|
+
"instance_type": "t3a.xlarge",
|
|
37
|
+
"state": "running",
|
|
38
|
+
"public_ip": "54.123.45.67",
|
|
39
|
+
"launch_time": (
|
|
40
|
+
datetime.now(timezone.utc) - timedelta(hours=2)
|
|
41
|
+
).isoformat(),
|
|
42
|
+
"idle_state": {
|
|
43
|
+
"is_idle": True,
|
|
44
|
+
"reason": "All sensors report idle",
|
|
45
|
+
"idle_seconds": 450,
|
|
46
|
+
"timeout_seconds": 1800,
|
|
47
|
+
"sensors": {
|
|
48
|
+
"coffee": {
|
|
49
|
+
"active": False,
|
|
50
|
+
"confidence": "HIGH",
|
|
51
|
+
"reason": "No coffee lock",
|
|
52
|
+
"details": {},
|
|
53
|
+
},
|
|
54
|
+
"ssh": {
|
|
55
|
+
"active": False,
|
|
56
|
+
"confidence": "HIGH",
|
|
57
|
+
"reason": "No active SSH sessions",
|
|
58
|
+
"details": {},
|
|
59
|
+
},
|
|
60
|
+
"ide": {
|
|
61
|
+
"active": False,
|
|
62
|
+
"confidence": "MEDIUM",
|
|
63
|
+
"reason": "No IDE connections detected (after 3 checks)",
|
|
64
|
+
"details": {},
|
|
65
|
+
},
|
|
66
|
+
"docker": {
|
|
67
|
+
"active": False,
|
|
68
|
+
"confidence": "MEDIUM",
|
|
69
|
+
"reason": "No workload containers",
|
|
70
|
+
"details": {"ignored": []}, # Empty list - should not display
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
"attached_studios": [],
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Scenario 2: Active with SSH and Docker
|
|
79
|
+
scenarios["active_ssh_docker"] = {
|
|
80
|
+
"name": "Active: SSH + Docker Workloads",
|
|
81
|
+
"status_data": {
|
|
82
|
+
"name": "bob-training",
|
|
83
|
+
"instance_id": "i-0fedcba987654321",
|
|
84
|
+
"instance_type": "g5.4xlarge",
|
|
85
|
+
"state": "running",
|
|
86
|
+
"public_ip": "34.234.56.78",
|
|
87
|
+
"launch_time": (
|
|
88
|
+
datetime.now(timezone.utc) - timedelta(minutes=45)
|
|
89
|
+
).isoformat(),
|
|
90
|
+
"idle_state": {
|
|
91
|
+
"is_idle": False,
|
|
92
|
+
"reason": "ssh: 2 SSH session(s), docker: 3 workload container(s)",
|
|
93
|
+
"idle_seconds": 0,
|
|
94
|
+
"timeout_seconds": 1800,
|
|
95
|
+
"sensors": {
|
|
96
|
+
"coffee": {
|
|
97
|
+
"active": False,
|
|
98
|
+
"confidence": "HIGH",
|
|
99
|
+
"reason": "No coffee lock",
|
|
100
|
+
"details": {},
|
|
101
|
+
},
|
|
102
|
+
"ssh": {
|
|
103
|
+
"active": True,
|
|
104
|
+
"confidence": "HIGH",
|
|
105
|
+
"reason": "2 SSH session(s)",
|
|
106
|
+
"details": {
|
|
107
|
+
"sessions": [
|
|
108
|
+
"bob pts/0 2025-11-13 14:30 old 12345",
|
|
109
|
+
"alice pts/1 2025-11-13 15:00 old 67890",
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
"ide": {
|
|
114
|
+
"active": False,
|
|
115
|
+
"confidence": "MEDIUM",
|
|
116
|
+
"reason": "No IDE connections detected",
|
|
117
|
+
"details": {},
|
|
118
|
+
},
|
|
119
|
+
"docker": {
|
|
120
|
+
"active": True,
|
|
121
|
+
"confidence": "MEDIUM",
|
|
122
|
+
"reason": "3 workload container(s)",
|
|
123
|
+
"details": {
|
|
124
|
+
"containers": [
|
|
125
|
+
"training-job-1",
|
|
126
|
+
"tensorboard",
|
|
127
|
+
"jupyter-lab",
|
|
128
|
+
],
|
|
129
|
+
"ignored": [
|
|
130
|
+
"ecs-agent (AWS system container)",
|
|
131
|
+
"devcontainer (dev-container)",
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"attached_studios": [{"user": "bob", "studio_id": "vol-0123456789abcdef0"}],
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Scenario 3: Active with IDE only
|
|
142
|
+
scenarios["active_ide"] = {
|
|
143
|
+
"name": "Active: IDE Connection Only",
|
|
144
|
+
"status_data": {
|
|
145
|
+
"name": "charlie-dev",
|
|
146
|
+
"instance_id": "i-0abc123def456789",
|
|
147
|
+
"instance_type": "t3a.xlarge",
|
|
148
|
+
"state": "running",
|
|
149
|
+
"public_ip": "52.12.34.56",
|
|
150
|
+
"launch_time": (
|
|
151
|
+
datetime.now(timezone.utc) - timedelta(hours=5)
|
|
152
|
+
).isoformat(),
|
|
153
|
+
"idle_state": {
|
|
154
|
+
"is_idle": False,
|
|
155
|
+
"reason": "ide: 2 IDE(s):",
|
|
156
|
+
"idle_seconds": 0,
|
|
157
|
+
"timeout_seconds": 1800,
|
|
158
|
+
"sensors": {
|
|
159
|
+
"coffee": {
|
|
160
|
+
"active": False,
|
|
161
|
+
"confidence": "HIGH",
|
|
162
|
+
"reason": "No coffee lock",
|
|
163
|
+
"details": {},
|
|
164
|
+
},
|
|
165
|
+
"ssh": {
|
|
166
|
+
"active": False,
|
|
167
|
+
"confidence": "HIGH",
|
|
168
|
+
"reason": "No SSH sessions",
|
|
169
|
+
"details": {},
|
|
170
|
+
},
|
|
171
|
+
"ide": {
|
|
172
|
+
"active": True,
|
|
173
|
+
"confidence": "MEDIUM",
|
|
174
|
+
"reason": "2 IDE(s):",
|
|
175
|
+
"details": {
|
|
176
|
+
"unique_flavor_count": 2,
|
|
177
|
+
"unique_pid_count": 3,
|
|
178
|
+
"flavors": ["cursor", "vscode"],
|
|
179
|
+
"connections": [
|
|
180
|
+
"PID 12345: node",
|
|
181
|
+
"PID 12346: code-server",
|
|
182
|
+
"PID 12347: cursor-server",
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
"docker": {
|
|
187
|
+
"active": False,
|
|
188
|
+
"confidence": "MEDIUM",
|
|
189
|
+
"reason": "No workload containers",
|
|
190
|
+
"details": {"ignored": ["ecs-agent (AWS system container)"]},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
"attached_studios": [
|
|
195
|
+
{"user": "charlie", "studio_id": "vol-09876543210fedcba"}
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Scenario 4: Coffee lock active (near timeout)
|
|
201
|
+
scenarios["coffee_lock"] = {
|
|
202
|
+
"name": "Active: Coffee Lock (Near Timeout)",
|
|
203
|
+
"status_data": {
|
|
204
|
+
"name": "diana-batch",
|
|
205
|
+
"instance_id": "i-0def789abc123456",
|
|
206
|
+
"instance_type": "c5.9xlarge",
|
|
207
|
+
"state": "running",
|
|
208
|
+
"public_ip": "18.234.56.78",
|
|
209
|
+
"launch_time": (
|
|
210
|
+
datetime.now(timezone.utc) - timedelta(hours=8)
|
|
211
|
+
).isoformat(),
|
|
212
|
+
"idle_state": {
|
|
213
|
+
"is_idle": False,
|
|
214
|
+
"reason": "coffee: Coffee lock active (15m remaining)",
|
|
215
|
+
"idle_seconds": 0,
|
|
216
|
+
"timeout_seconds": 1800,
|
|
217
|
+
"sensors": {
|
|
218
|
+
"coffee": {
|
|
219
|
+
"active": True,
|
|
220
|
+
"confidence": "HIGH",
|
|
221
|
+
"reason": "Coffee lock active (15m remaining)",
|
|
222
|
+
"details": {
|
|
223
|
+
"expires_at": (datetime.now().timestamp() + 900) # 15 min
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
"ssh": {
|
|
227
|
+
"active": False,
|
|
228
|
+
"confidence": "HIGH",
|
|
229
|
+
"reason": "No SSH sessions",
|
|
230
|
+
"details": {},
|
|
231
|
+
},
|
|
232
|
+
"ide": {
|
|
233
|
+
"active": False,
|
|
234
|
+
"confidence": "MEDIUM",
|
|
235
|
+
"reason": "No IDE connections detected",
|
|
236
|
+
"details": {},
|
|
237
|
+
},
|
|
238
|
+
"docker": {
|
|
239
|
+
"active": False,
|
|
240
|
+
"confidence": "MEDIUM",
|
|
241
|
+
"reason": "No workload containers",
|
|
242
|
+
"details": {},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
"attached_studios": [],
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Scenario 5: Almost timed out
|
|
251
|
+
scenarios["near_timeout"] = {
|
|
252
|
+
"name": "Nearly Timed Out (28 min idle)",
|
|
253
|
+
"status_data": {
|
|
254
|
+
"name": "eve-forgotten",
|
|
255
|
+
"instance_id": "i-0eeeeeeeeeeeeeeee",
|
|
256
|
+
"instance_type": "t3a.xlarge",
|
|
257
|
+
"state": "running",
|
|
258
|
+
"public_ip": "54.234.56.89",
|
|
259
|
+
"launch_time": (
|
|
260
|
+
datetime.now(timezone.utc) - timedelta(hours=3)
|
|
261
|
+
).isoformat(),
|
|
262
|
+
"idle_state": {
|
|
263
|
+
"is_idle": True,
|
|
264
|
+
"reason": "All sensors report idle",
|
|
265
|
+
"idle_seconds": 1680, # 28 minutes
|
|
266
|
+
"timeout_seconds": 1800, # 30 minutes
|
|
267
|
+
"sensors": {
|
|
268
|
+
"coffee": {
|
|
269
|
+
"active": False,
|
|
270
|
+
"confidence": "HIGH",
|
|
271
|
+
"reason": "No coffee lock",
|
|
272
|
+
"details": {},
|
|
273
|
+
},
|
|
274
|
+
"ssh": {
|
|
275
|
+
"active": False,
|
|
276
|
+
"confidence": "HIGH",
|
|
277
|
+
"reason": "No SSH sessions",
|
|
278
|
+
"details": {},
|
|
279
|
+
},
|
|
280
|
+
"ide": {
|
|
281
|
+
"active": False,
|
|
282
|
+
"confidence": "MEDIUM",
|
|
283
|
+
"reason": "No IDE connections detected",
|
|
284
|
+
"details": {},
|
|
285
|
+
},
|
|
286
|
+
"docker": {
|
|
287
|
+
"active": False,
|
|
288
|
+
"confidence": "MEDIUM",
|
|
289
|
+
"reason": "No workload containers",
|
|
290
|
+
"details": {},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
"attached_studios": [{"user": "eve", "studio_id": "vol-0eeeeeeeeeeeeeeee"}],
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Scenario 6: Initializing (not ready yet)
|
|
299
|
+
scenarios["initializing"] = {
|
|
300
|
+
"name": "Engine Initializing (Not Ready)",
|
|
301
|
+
"status_data": {
|
|
302
|
+
"name": "frank-new",
|
|
303
|
+
"instance_id": "i-0fffffffffffffff",
|
|
304
|
+
"instance_type": "g5.xlarge",
|
|
305
|
+
"state": "running",
|
|
306
|
+
"public_ip": "3.123.45.67",
|
|
307
|
+
"launch_time": (
|
|
308
|
+
datetime.now(timezone.utc) - timedelta(minutes=2)
|
|
309
|
+
).isoformat(),
|
|
310
|
+
"readiness": {
|
|
311
|
+
"ready": False,
|
|
312
|
+
"status": "configuring",
|
|
313
|
+
"current_stage": "installing_packages",
|
|
314
|
+
"progress_percent": 50,
|
|
315
|
+
"estimated_time_remaining_seconds": 90,
|
|
316
|
+
},
|
|
317
|
+
"idle_state": None, # Not available yet
|
|
318
|
+
"attached_studios": [],
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Scenario 7: Stopped engine
|
|
323
|
+
scenarios["stopped"] = {
|
|
324
|
+
"name": "Stopped Engine",
|
|
325
|
+
"status_data": {
|
|
326
|
+
"name": "george-stopped",
|
|
327
|
+
"instance_id": "i-0aaaaaaaaaaaaaaaa",
|
|
328
|
+
"instance_type": "t3a.xlarge",
|
|
329
|
+
"state": "stopped",
|
|
330
|
+
"public_ip": None,
|
|
331
|
+
"launch_time": (
|
|
332
|
+
datetime.now(timezone.utc) - timedelta(hours=4)
|
|
333
|
+
).isoformat(),
|
|
334
|
+
"idle_state": {
|
|
335
|
+
"is_idle": True,
|
|
336
|
+
"reason": "All sensors report idle",
|
|
337
|
+
"idle_seconds": 1800,
|
|
338
|
+
"timeout_seconds": 1800,
|
|
339
|
+
"sensors": {
|
|
340
|
+
"coffee": {
|
|
341
|
+
"active": False,
|
|
342
|
+
"confidence": "HIGH",
|
|
343
|
+
"reason": "No coffee lock",
|
|
344
|
+
"details": {},
|
|
345
|
+
},
|
|
346
|
+
"ssh": {
|
|
347
|
+
"active": False,
|
|
348
|
+
"confidence": "HIGH",
|
|
349
|
+
"reason": "No SSH sessions",
|
|
350
|
+
"details": {},
|
|
351
|
+
},
|
|
352
|
+
"ide": {
|
|
353
|
+
"active": False,
|
|
354
|
+
"confidence": "MEDIUM",
|
|
355
|
+
"reason": "No IDE connections",
|
|
356
|
+
"details": {},
|
|
357
|
+
},
|
|
358
|
+
"docker": {
|
|
359
|
+
"active": False,
|
|
360
|
+
"confidence": "MEDIUM",
|
|
361
|
+
"reason": "No workload containers",
|
|
362
|
+
"details": {},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
"attached_studios": [],
|
|
367
|
+
},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return scenarios
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def display_scenario(name, scenario_data, detailed=True):
|
|
374
|
+
"""Display a single scenario in formatted output."""
|
|
375
|
+
status = scenario_data["status_data"]
|
|
376
|
+
|
|
377
|
+
print(f"\n{'='*80}")
|
|
378
|
+
print(f" SCENARIO: {scenario_data['name']}")
|
|
379
|
+
print(f"{'='*80}\n")
|
|
380
|
+
|
|
381
|
+
# Basic info
|
|
382
|
+
print(f"Engine: \033[34m{status['name']}\033[0m") # Blue engine name
|
|
383
|
+
print(f"Instance ID: {status['instance_id']}")
|
|
384
|
+
print(f"Type: {status['instance_type']}")
|
|
385
|
+
|
|
386
|
+
# Show state in red if stopped, normal otherwise
|
|
387
|
+
engine_state = status["state"]
|
|
388
|
+
if engine_state.lower() in ["stopped", "stopping", "terminated", "terminating"]:
|
|
389
|
+
print(f"State: \033[31m{engine_state}\033[0m") # Red for stopped
|
|
390
|
+
else:
|
|
391
|
+
print(f"State: {engine_state}")
|
|
392
|
+
|
|
393
|
+
if status.get("public_ip"):
|
|
394
|
+
print(f"Public IP: {status['public_ip']}")
|
|
395
|
+
|
|
396
|
+
if status.get("launch_time"):
|
|
397
|
+
print(f"Launched: {format_time_ago(status['launch_time'])}")
|
|
398
|
+
|
|
399
|
+
# Check if engine is stopped - don't show idle state or activity sensors
|
|
400
|
+
if engine_state.lower() in ["stopped", "stopping", "terminated", "terminating"]:
|
|
401
|
+
print() # Extra newline for readability
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Show readiness if not ready
|
|
405
|
+
if status.get("readiness") and not status["readiness"].get("ready"):
|
|
406
|
+
readiness = status["readiness"]
|
|
407
|
+
print(f"\n⏳ Initialization: {readiness.get('progress_percent', 0)}%")
|
|
408
|
+
print(f"Current Stage: {readiness.get('current_stage', 'unknown')}")
|
|
409
|
+
if readiness.get("estimated_time_remaining_seconds"):
|
|
410
|
+
remaining = readiness["estimated_time_remaining_seconds"]
|
|
411
|
+
print(f"Estimated Time Remaining: {remaining}s")
|
|
412
|
+
|
|
413
|
+
# Show idle state (only for running engines)
|
|
414
|
+
if status.get("idle_state"):
|
|
415
|
+
attached_studios = status.get("attached_studios", [])
|
|
416
|
+
print(
|
|
417
|
+
f"\n{format_idle_state(status['idle_state'], detailed=detailed, attached_studios=attached_studios)}"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
print() # Extra newline for readability
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def main():
|
|
424
|
+
parser = argparse.ArgumentParser(
|
|
425
|
+
description="Simulator for engine status output design iteration"
|
|
426
|
+
)
|
|
427
|
+
parser.add_argument(
|
|
428
|
+
"--scenario",
|
|
429
|
+
help="Show specific scenario (idle, active_ssh_docker, active_ide, coffee_lock, near_timeout, initializing, stopped)",
|
|
430
|
+
type=str,
|
|
431
|
+
)
|
|
432
|
+
parser.add_argument(
|
|
433
|
+
"--simple", action="store_true", help="Show simple (non-detailed) output"
|
|
434
|
+
)
|
|
435
|
+
parser.add_argument(
|
|
436
|
+
"--colorful",
|
|
437
|
+
action="store_true",
|
|
438
|
+
help="Use more emojis and colors (future: implement enhanced formatting)",
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
args = parser.parse_args()
|
|
442
|
+
|
|
443
|
+
scenarios = generate_scenarios()
|
|
444
|
+
|
|
445
|
+
if args.scenario:
|
|
446
|
+
if args.scenario not in scenarios:
|
|
447
|
+
print(f"❌ Unknown scenario: {args.scenario}")
|
|
448
|
+
print(f"Available scenarios: {', '.join(scenarios.keys())}")
|
|
449
|
+
return 1
|
|
450
|
+
|
|
451
|
+
display_scenario(
|
|
452
|
+
args.scenario, scenarios[args.scenario], detailed=not args.simple
|
|
453
|
+
)
|
|
454
|
+
else:
|
|
455
|
+
# Show all scenarios
|
|
456
|
+
print("\n" + "=" * 80)
|
|
457
|
+
print(" ENGINE STATUS OUTPUT SIMULATOR")
|
|
458
|
+
print("=" * 80)
|
|
459
|
+
print("\nShowing all scenarios. Use --scenario <name> to see just one.")
|
|
460
|
+
print("Use --simple to see non-detailed output.")
|
|
461
|
+
print()
|
|
462
|
+
|
|
463
|
+
for name, scenario_data in scenarios.items():
|
|
464
|
+
display_scenario(name, scenario_data, detailed=not args.simple)
|
|
465
|
+
|
|
466
|
+
if args.colorful:
|
|
467
|
+
print("\n💡 TIP: --colorful flag noted! Implement enhanced formatting by:")
|
|
468
|
+
print(" 1. Edit format_idle_state() in progress.py")
|
|
469
|
+
print(" 2. Re-run this simulator to see changes")
|
|
470
|
+
print(" 3. No AWS calls needed - iterate quickly!\n")
|
|
471
|
+
|
|
472
|
+
return 0
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if __name__ == "__main__":
|
|
476
|
+
sys.exit(main())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Standalone utilities for simulators - no external dependencies."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_time_ago(timestamp: str) -> str:
|
|
8
|
+
"""Format timestamp as time ago.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
timestamp: ISO format timestamp
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Human readable time ago string
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
18
|
+
now = datetime.now(dt.tzinfo)
|
|
19
|
+
delta = now - dt
|
|
20
|
+
|
|
21
|
+
seconds = int(delta.total_seconds())
|
|
22
|
+
if seconds < 60:
|
|
23
|
+
return f"{seconds}s ago"
|
|
24
|
+
elif seconds < 3600:
|
|
25
|
+
return f"{seconds // 60}m ago"
|
|
26
|
+
elif seconds < 86400:
|
|
27
|
+
return f"{seconds // 3600}h ago"
|
|
28
|
+
else:
|
|
29
|
+
return f"{seconds // 86400}d ago"
|
|
30
|
+
except:
|
|
31
|
+
return timestamp
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def format_sensor_status(sensor_data: Dict[str, Any]) -> str:
|
|
35
|
+
"""Format sensor status for display.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
sensor_data: Sensor data dict
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Formatted string
|
|
42
|
+
"""
|
|
43
|
+
active = sensor_data.get("active", False)
|
|
44
|
+
reason = sensor_data.get("reason", "No reason provided")
|
|
45
|
+
|
|
46
|
+
if active:
|
|
47
|
+
return f"🟢\n {reason}"
|
|
48
|
+
else:
|
|
49
|
+
return "⚪"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_idle_state(
|
|
53
|
+
idle_state: Dict[str, Any],
|
|
54
|
+
detailed: bool = True,
|
|
55
|
+
attached_studios: Optional[list] = None,
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Format idle state for display.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
idle_state: Idle state dict
|
|
61
|
+
detailed: Whether to show detailed sensor information
|
|
62
|
+
attached_studios: Optional list of attached studio dicts
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Formatted string
|
|
66
|
+
"""
|
|
67
|
+
is_idle = idle_state.get("is_idle", False)
|
|
68
|
+
|
|
69
|
+
lines = []
|
|
70
|
+
|
|
71
|
+
# Status line
|
|
72
|
+
icon = "🟡 IDLE" if is_idle else "🟢 ACTIVE"
|
|
73
|
+
lines.append(f"Idle Status: {icon}")
|
|
74
|
+
|
|
75
|
+
# Timing information
|
|
76
|
+
if idle_state.get("idle_seconds"):
|
|
77
|
+
timeout = int(idle_state.get("timeout_seconds", 1800))
|
|
78
|
+
elapsed = idle_state["idle_seconds"]
|
|
79
|
+
remaining = max(0, timeout - elapsed)
|
|
80
|
+
lines.append(f"Idle Time: {elapsed}s / {timeout}s")
|
|
81
|
+
if remaining > 0:
|
|
82
|
+
minutes = remaining // 60
|
|
83
|
+
# Yellow text using ANSI escape codes
|
|
84
|
+
lines.append(f"\033[33mWill shutdown in: {remaining}s ({minutes}m)\033[0m")
|
|
85
|
+
|
|
86
|
+
# Attached studios (show before sensors)
|
|
87
|
+
if attached_studios:
|
|
88
|
+
# Purple text for studio names
|
|
89
|
+
studio_names = ", ".join(
|
|
90
|
+
[
|
|
91
|
+
f"\033[35m{s.get('user', s.get('studio_id', 'unknown'))}\033[0m"
|
|
92
|
+
for s in attached_studios
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
lines.append(f"\nAttached Studios: {studio_names}")
|
|
96
|
+
else:
|
|
97
|
+
# Normal text for "None"
|
|
98
|
+
lines.append(f"\nAttached Studios: None")
|
|
99
|
+
|
|
100
|
+
# Detailed sensor information with colorful emojis
|
|
101
|
+
if detailed and idle_state.get("sensors"):
|
|
102
|
+
lines.append(f"\n{'═'*60}")
|
|
103
|
+
lines.append("🔍 Activity Sensors:")
|
|
104
|
+
lines.append(f"{'═'*60}")
|
|
105
|
+
|
|
106
|
+
# Sensor emoji mapping
|
|
107
|
+
sensor_emojis = {
|
|
108
|
+
"coffee": "☕",
|
|
109
|
+
"ssh": "🐚",
|
|
110
|
+
"ide": "💻",
|
|
111
|
+
"docker": "🐳",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for sensor_name, sensor_data in idle_state["sensors"].items():
|
|
115
|
+
emoji = sensor_emojis.get(sensor_name.lower(), "📊")
|
|
116
|
+
active = sensor_data.get("active", False)
|
|
117
|
+
|
|
118
|
+
# Special formatting for coffee sensor
|
|
119
|
+
if sensor_name.lower() == "coffee" and active:
|
|
120
|
+
# Extract minutes from details for cleaner display
|
|
121
|
+
details = sensor_data.get("details", {})
|
|
122
|
+
remaining_seconds = int(details.get("remaining_seconds", 0))
|
|
123
|
+
remaining_minutes = remaining_seconds // 60
|
|
124
|
+
lines.append(f"\n{emoji} {sensor_name.upper()} 🟢")
|
|
125
|
+
lines.append(f" Caffeinated for another {remaining_minutes}m")
|
|
126
|
+
else:
|
|
127
|
+
status_icon = format_sensor_status(sensor_data)
|
|
128
|
+
# Format: emoji NAME status_icon (on same line for inactive, split for active)
|
|
129
|
+
if active:
|
|
130
|
+
lines.append(
|
|
131
|
+
f"\n{emoji} {sensor_name.upper()} {status_icon.split(chr(10))[0]}"
|
|
132
|
+
)
|
|
133
|
+
# Add reason on next line
|
|
134
|
+
reason_line = (
|
|
135
|
+
status_icon.split("\n")[1] if "\n" in status_icon else ""
|
|
136
|
+
)
|
|
137
|
+
if reason_line:
|
|
138
|
+
lines.append(reason_line)
|
|
139
|
+
else:
|
|
140
|
+
lines.append(f"\n{emoji} {sensor_name.upper()} {status_icon}")
|
|
141
|
+
|
|
142
|
+
# Show details if available (skip for active coffee sensor with special formatting)
|
|
143
|
+
if sensor_name.lower() == "coffee" and active:
|
|
144
|
+
continue # Already showed coffee details in special format above
|
|
145
|
+
|
|
146
|
+
details = sensor_data.get("details", {})
|
|
147
|
+
if details:
|
|
148
|
+
for key, value in details.items():
|
|
149
|
+
# Skip internal bookkeeping fields and redundant info
|
|
150
|
+
if key in [
|
|
151
|
+
"unique_flavor_count",
|
|
152
|
+
"unique_pid_count",
|
|
153
|
+
"expires_at",
|
|
154
|
+
"flavors",
|
|
155
|
+
"remaining_seconds", # Redundant with time shown in reason
|
|
156
|
+
"pid_count", # Redundant with connections list
|
|
157
|
+
"connections", # Redundant, connections shown in sessions/containers
|
|
158
|
+
]:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
if isinstance(value, list):
|
|
162
|
+
if value: # Only show non-empty lists
|
|
163
|
+
# All lists shown with bullets, no header for containers/connections/sessions
|
|
164
|
+
if key in ["containers", "connections", "sessions"]:
|
|
165
|
+
# Just show the items with bullets, no header
|
|
166
|
+
for item in value[:5]:
|
|
167
|
+
lines.append(f" • {item}")
|
|
168
|
+
elif key == "ignored":
|
|
169
|
+
# Ignored list at same indentation level, with header
|
|
170
|
+
lines.append(f" {key}:")
|
|
171
|
+
for item in value[:5]:
|
|
172
|
+
lines.append(f" • {item}")
|
|
173
|
+
else:
|
|
174
|
+
# Other lists get shown with bullets only
|
|
175
|
+
for item in value[:5]:
|
|
176
|
+
lines.append(f" • {item}")
|
|
177
|
+
elif not isinstance(value, (dict, list)):
|
|
178
|
+
lines.append(f" ℹ️ {key}: {value}")
|
|
179
|
+
|
|
180
|
+
return "\n".join(lines)
|