runtime-sdk 0.4.24__tar.gz → 0.4.25__tar.gz
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.
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/PKG-INFO +1 -1
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/pyproject.toml +1 -1
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk/cli.py +93 -156
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk.egg-info/PKG-INFO +1 -1
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/README.md +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk/__init__.py +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk/client.py +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk/config.py +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk.egg-info/SOURCES.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk.egg-info/dependency_links.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk.egg-info/entry_points.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk.egg-info/requires.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/scripts/runtime_sdk.egg-info/top_level.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.25}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.25
|
|
4
4
|
Summary: Runtime Python SDK and CLI
|
|
5
5
|
Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
|
|
6
6
|
Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
|
|
@@ -889,22 +889,6 @@ def _prompt_typed_confirm(message: str, expected: str) -> bool:
|
|
|
889
889
|
return typed.strip().lower() == expected.strip().lower()
|
|
890
890
|
|
|
891
891
|
|
|
892
|
-
def _display_width(value: Any, minimum: int, maximum: int) -> int:
|
|
893
|
-
text = str(value or "")
|
|
894
|
-
return max(minimum, min(len(text), maximum))
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
def _fit_cell(value: Any, width: int) -> str:
|
|
898
|
-
text = str(value or "")
|
|
899
|
-
if width <= 0:
|
|
900
|
-
return ""
|
|
901
|
-
if len(text) > width:
|
|
902
|
-
if width == 1:
|
|
903
|
-
return "…"
|
|
904
|
-
text = text[: width - 1] + "…"
|
|
905
|
-
return text.ljust(width)
|
|
906
|
-
|
|
907
|
-
|
|
908
892
|
def _computer_power_state(c: dict[str, Any]) -> str:
|
|
909
893
|
power = c.get("power_state")
|
|
910
894
|
if power is not None:
|
|
@@ -1123,157 +1107,97 @@ def _computer_sort_key(c: dict[str, Any]) -> tuple[int, str, str]:
|
|
|
1123
1107
|
|
|
1124
1108
|
|
|
1125
1109
|
|
|
1126
|
-
def
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
url_budget = max(16, term_width - reserved)
|
|
1139
|
-
url_width = max(
|
|
1140
|
-
len("url"),
|
|
1141
|
-
min(max(len(_computer_url_label(c)) for c in computers), url_budget),
|
|
1142
|
-
)
|
|
1143
|
-
return {
|
|
1144
|
-
"slug": slug_width,
|
|
1145
|
-
"status": status_width,
|
|
1146
|
-
"public_url": url_width,
|
|
1147
|
-
"created_at": created_width,
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
def _format_computer_row(c: dict[str, Any], widths: dict[str, int]) -> str:
|
|
1152
|
-
return " │ ".join(
|
|
1153
|
-
[
|
|
1154
|
-
_fit_cell(_computer_name(c), widths["slug"]),
|
|
1155
|
-
_fit_cell(_computer_status_label(c), widths["status"]),
|
|
1156
|
-
_fit_cell(_computer_url_label(c), widths["public_url"]),
|
|
1157
|
-
_fit_cell(_computer_created_label(c), widths["created_at"]),
|
|
1158
|
-
]
|
|
1159
|
-
)
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
def _computer_table_prompt(prompt: str, computers: list[dict[str, Any]]) -> tuple[str, dict[str, int]]:
|
|
1163
|
-
widths = _computer_table_layout(computers)
|
|
1164
|
-
header = " │ ".join(
|
|
1165
|
-
[
|
|
1166
|
-
_fit_cell("name", widths["slug"]),
|
|
1167
|
-
_fit_cell("state", widths["status"]),
|
|
1168
|
-
_fit_cell("url", widths["public_url"]),
|
|
1169
|
-
_fit_cell("age", widths["created_at"]),
|
|
1170
|
-
]
|
|
1171
|
-
)
|
|
1172
|
-
divider = "─" * len(header)
|
|
1173
|
-
return f"{prompt}\n{header}\n{divider}", widths
|
|
1110
|
+
def _picker_status_color(status: str) -> str:
|
|
1111
|
+
if "cold" in status:
|
|
1112
|
+
return "fg:#60a5fa" # cyan-blue
|
|
1113
|
+
if "warm" in status or "running" in status:
|
|
1114
|
+
return "fg:#22c55e" # green
|
|
1115
|
+
if "archiving" in status or "restoring" in status:
|
|
1116
|
+
return "fg:#eab308" # yellow
|
|
1117
|
+
if "creating" in status:
|
|
1118
|
+
return "fg:#a855f7" # magenta
|
|
1119
|
+
if "orphaned" in status:
|
|
1120
|
+
return "fg:#ef4444" # red
|
|
1121
|
+
return "fg:#e5e7eb"
|
|
1174
1122
|
|
|
1175
1123
|
|
|
1124
|
+
def _picker_widths(computers: list[dict[str, Any]]) -> dict[str, int]:
|
|
1125
|
+
"""Compute aligned column widths for the interactive picker rows.
|
|
1176
1126
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1127
|
+
We budget total width to leave room for questionary's pointer + indicator.
|
|
1128
|
+
"""
|
|
1129
|
+
term_width = shutil.get_terminal_size((100, 20)).columns
|
|
1130
|
+
name_w = max(len("name"), max((len(_computer_name(c)) for c in computers), default=0))
|
|
1131
|
+
name_w = min(name_w, 24)
|
|
1132
|
+
status_w = max(len("state"), max((len(_computer_status_label(c)) for c in computers), default=0))
|
|
1133
|
+
status_w = min(status_w, 14)
|
|
1134
|
+
age_w = max(len("age"), max((len(_computer_created_label(c)) for c in computers), default=0))
|
|
1135
|
+
age_w = min(age_w, 6)
|
|
1136
|
+
# pointer + indicator + 2 gaps * 3 + status dot (2 chars) + padding
|
|
1137
|
+
reserved = 6 + name_w + status_w + age_w + 2 * 3 + 2
|
|
1138
|
+
url_w = max(len("url"), min(max((len(_computer_url_label(c)) for c in computers), default=16), term_width - reserved))
|
|
1139
|
+
url_w = max(10, url_w)
|
|
1140
|
+
return {"name": name_w, "status": status_w, "url": url_w, "age": age_w}
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _computer_choice_title(c: dict[str, Any], widths: dict[str, int]) -> list[tuple[str, str]]:
|
|
1144
|
+
"""Return a styled list-of-tuples title so the picker rows look tabular.
|
|
1145
|
+
|
|
1146
|
+
Columns: name · state (colored dot + label) · url · age.
|
|
1147
|
+
"""
|
|
1148
|
+
name = _computer_name(c)
|
|
1179
1149
|
status = _computer_status_label(c)
|
|
1180
|
-
url = _computer_url_label(c)
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
body = Text()
|
|
1200
|
-
body.append(f"total {len(computers)}", style="bold white")
|
|
1201
|
-
if counts:
|
|
1202
|
-
body.append(" ")
|
|
1203
|
-
first = True
|
|
1204
|
-
status_styles = {
|
|
1205
|
-
"warm": "bold green",
|
|
1206
|
-
"cold": "bold cyan",
|
|
1207
|
-
"running": "bold green",
|
|
1208
|
-
"archiving": "bold yellow",
|
|
1209
|
-
"restoring": "bold yellow",
|
|
1210
|
-
"creating": "bold magenta",
|
|
1211
|
-
"orphaned": "bold red",
|
|
1212
|
-
}
|
|
1213
|
-
for status in sorted(counts):
|
|
1214
|
-
if not first:
|
|
1215
|
-
body.append(" • ", style="dim")
|
|
1216
|
-
first = False
|
|
1217
|
-
status_style = "bold white"
|
|
1218
|
-
for token, style in status_styles.items():
|
|
1219
|
-
if token in status:
|
|
1220
|
-
status_style = style
|
|
1221
|
-
break
|
|
1222
|
-
body.append(status, style=status_style)
|
|
1223
|
-
body.append(f" {counts[status]}", style="white")
|
|
1224
|
-
|
|
1225
|
-
_UI.console().print(
|
|
1226
|
-
Panel(body, title="runtime", border_style="blue", expand=False, padding=(0, 1))
|
|
1227
|
-
)
|
|
1228
|
-
|
|
1150
|
+
url = _computer_url_label(c) or "—"
|
|
1151
|
+
age = _computer_created_label(c) or ""
|
|
1152
|
+
color = _picker_status_color(status)
|
|
1153
|
+
dot = _computer_status_dot(status)
|
|
1154
|
+
# Keep each column within its budget; truncate with ellipsis on overflow.
|
|
1155
|
+
def fit(text: str, width: int, align: str = "<") -> str:
|
|
1156
|
+
text = str(text or "")
|
|
1157
|
+
if len(text) > width:
|
|
1158
|
+
return text[: max(1, width - 1)] + "…"
|
|
1159
|
+
return format(text, f"{align}{width}")
|
|
1160
|
+
return [
|
|
1161
|
+
("", fit(name, widths["name"]) + " "),
|
|
1162
|
+
(color, f"{dot} "),
|
|
1163
|
+
(color, fit(status, widths["status"])),
|
|
1164
|
+
("", " "),
|
|
1165
|
+
("fg:#cbd5e1", fit(url, widths["url"])),
|
|
1166
|
+
("", " "),
|
|
1167
|
+
("fg:#6b7280", fit(age, widths["age"], ">")),
|
|
1168
|
+
]
|
|
1229
1169
|
|
|
1230
1170
|
|
|
1231
|
-
def _render_computer_table(computers: list[dict[str, Any]], *, title: str | None = None) -> None:
|
|
1232
|
-
mods = _UI.rich()
|
|
1233
|
-
Table, box = mods["Table"], mods["box"]
|
|
1234
|
-
table = Table(
|
|
1235
|
-
title=title,
|
|
1236
|
-
box=box.MINIMAL_DOUBLE_HEAD,
|
|
1237
|
-
expand=True,
|
|
1238
|
-
show_lines=False,
|
|
1239
|
-
header_style="bold #7dd3fc",
|
|
1240
|
-
title_style="bold white",
|
|
1241
|
-
pad_edge=False,
|
|
1242
|
-
collapse_padding=True,
|
|
1243
|
-
row_styles=["", "dim"],
|
|
1244
|
-
)
|
|
1245
|
-
table.add_column("Name", style="bold white", no_wrap=True, ratio=2)
|
|
1246
|
-
table.add_column("State", no_wrap=True, ratio=2)
|
|
1247
|
-
table.add_column("URL", style="white", overflow="fold", ratio=4)
|
|
1248
|
-
table.add_column("Age", style="dim", no_wrap=True, justify="right", ratio=1)
|
|
1249
|
-
|
|
1250
|
-
for computer in sorted(computers, key=_computer_sort_key):
|
|
1251
|
-
status = _computer_status_label(computer)
|
|
1252
|
-
status_style = _computer_status_style(status)
|
|
1253
|
-
table.add_row(
|
|
1254
|
-
_computer_name(computer),
|
|
1255
|
-
f"[{status_style}]{_computer_status_dot(status)} {status}[/{status_style}]",
|
|
1256
|
-
_computer_url_label(computer),
|
|
1257
|
-
_computer_created_label(computer),
|
|
1258
|
-
)
|
|
1259
1171
|
|
|
1260
|
-
_UI.console().print(table)
|
|
1261
1172
|
|
|
1262
1173
|
|
|
1263
1174
|
def _select_prompt(message: str, choices: list[Any], *, instruction: str | None = None) -> Any:
|
|
1264
1175
|
# questionary rejects use_jk_keys together with use_search_filter because
|
|
1265
1176
|
# j/k would be swallowed by the search prefix. We pick search over vim keys.
|
|
1266
|
-
|
|
1177
|
+
question = _UI.q().select(
|
|
1267
1178
|
message,
|
|
1268
1179
|
choices=choices,
|
|
1269
1180
|
qmark="●",
|
|
1270
1181
|
pointer="❯",
|
|
1271
|
-
instruction=instruction or "↑↓
|
|
1182
|
+
instruction=instruction or "↑↓ · type to search · enter select · esc back",
|
|
1272
1183
|
use_indicator=True,
|
|
1273
1184
|
use_jk_keys=False,
|
|
1274
1185
|
use_search_filter=True,
|
|
1275
1186
|
style=_UI.style(),
|
|
1276
1187
|
)
|
|
1188
|
+
# Bind ESC explicitly — questionary's select prompt does not bind it by
|
|
1189
|
+
# default. With eager=True the binding fires before prompt_toolkit's
|
|
1190
|
+
# meta-key timeout. .unsafe_ask() returns the `result` we pass to exit().
|
|
1191
|
+
with contextlib.suppress(Exception):
|
|
1192
|
+
from prompt_toolkit.keys import Keys
|
|
1193
|
+
|
|
1194
|
+
bindings = question.application.key_bindings
|
|
1195
|
+
|
|
1196
|
+
@bindings.add(Keys.Escape, eager=True)
|
|
1197
|
+
def _escape(event: Any) -> None:
|
|
1198
|
+
event.app.exit(result=None)
|
|
1199
|
+
|
|
1200
|
+
return question
|
|
1277
1201
|
|
|
1278
1202
|
|
|
1279
1203
|
def _pick_computer(
|
|
@@ -1283,8 +1207,8 @@ def _pick_computer(
|
|
|
1283
1207
|
) -> dict[str, Any] | None:
|
|
1284
1208
|
"""Fetch computers and let the user arrow-key through them.
|
|
1285
1209
|
|
|
1286
|
-
Returns the picked computer dict, or None
|
|
1287
|
-
|
|
1210
|
+
Returns the picked computer dict, or None on ESC / "← cancel".
|
|
1211
|
+
Ctrl-C propagates as KeyboardInterrupt.
|
|
1288
1212
|
"""
|
|
1289
1213
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1290
1214
|
payload = client.list_computers()
|
|
@@ -1299,8 +1223,7 @@ def _pick_computer(
|
|
|
1299
1223
|
return None
|
|
1300
1224
|
|
|
1301
1225
|
questionary = _UI.q()
|
|
1302
|
-
widths =
|
|
1303
|
-
_render_computer_table(computers)
|
|
1226
|
+
widths = _picker_widths(computers)
|
|
1304
1227
|
choices = [
|
|
1305
1228
|
questionary.Choice(title=_computer_choice_title(c, widths), value=c) for c in computers
|
|
1306
1229
|
]
|
|
@@ -2977,7 +2900,11 @@ def _send_terminal_resize(ws: Any) -> None:
|
|
|
2977
2900
|
|
|
2978
2901
|
|
|
2979
2902
|
def _interactive_list(config: RuntimeConfig) -> int:
|
|
2980
|
-
"""
|
|
2903
|
+
"""Interactive top-level menu: picker is the display, no separate table.
|
|
2904
|
+
|
|
2905
|
+
ESC / Ctrl+C / "✕ quit" exit. "+ new computer" creates one. "↻ refresh"
|
|
2906
|
+
re-fetches. Picking a VM drops into its action submenu.
|
|
2907
|
+
"""
|
|
2981
2908
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2982
2909
|
questionary = _UI.q()
|
|
2983
2910
|
|
|
@@ -2992,9 +2919,19 @@ def _interactive_list(config: RuntimeConfig) -> int:
|
|
|
2992
2919
|
return handle_create(config, None, None, None, None)
|
|
2993
2920
|
|
|
2994
2921
|
computers = sorted(computers, key=_computer_sort_key)
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2922
|
+
widths = _picker_widths(computers)
|
|
2923
|
+
|
|
2924
|
+
# Title summarises counts so the picker alone carries enough context.
|
|
2925
|
+
total = len(computers)
|
|
2926
|
+
warm = sum(1 for c in computers if "warm" in _computer_status_label(c) or "running" in _computer_status_label(c))
|
|
2927
|
+
cold = sum(1 for c in computers if "cold" in _computer_status_label(c))
|
|
2928
|
+
bits = [f"{total} total"]
|
|
2929
|
+
if warm:
|
|
2930
|
+
bits.append(f"{warm} warm")
|
|
2931
|
+
if cold:
|
|
2932
|
+
bits.append(f"{cold} cold")
|
|
2933
|
+
summary = " · ".join(bits)
|
|
2934
|
+
|
|
2998
2935
|
choices: list[Any] = [
|
|
2999
2936
|
questionary.Choice(title=_computer_choice_title(c, widths), value=c)
|
|
3000
2937
|
for c in computers
|
|
@@ -3004,9 +2941,9 @@ def _interactive_list(config: RuntimeConfig) -> int:
|
|
|
3004
2941
|
choices.append(questionary.Choice(title="↻ refresh", value="__refresh__"))
|
|
3005
2942
|
choices.append(questionary.Choice(title="✕ quit", value="__quit__"))
|
|
3006
2943
|
|
|
3007
|
-
picked = _select_prompt("Pick a computer", choices).unsafe_ask()
|
|
2944
|
+
picked = _select_prompt(f"Pick a computer ({summary})", choices).unsafe_ask()
|
|
3008
2945
|
|
|
3009
|
-
if picked == "__quit__":
|
|
2946
|
+
if picked is None or picked == "__quit__":
|
|
3010
2947
|
return 0
|
|
3011
2948
|
if picked == "__refresh__":
|
|
3012
2949
|
continue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.25
|
|
4
4
|
Summary: Runtime Python SDK and CLI
|
|
5
5
|
Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
|
|
6
6
|
Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|