twisk 0.1.0b0__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.
- twisk/__init__.py +3 -0
- twisk/backend/__init__.py +39 -0
- twisk/backend/base.py +284 -0
- twisk/backend/fake.py +629 -0
- twisk/backend/skypilot.py +729 -0
- twisk/cli.py +306 -0
- twisk/sandbox.py +471 -0
- twisk/settings.py +99 -0
- twisk/specs/__init__.py +26 -0
- twisk/specs/models.py +691 -0
- twisk/specs/store.py +193 -0
- twisk/templates/__init__.py +20 -0
- twisk/templates/builtin/cluster.yaml +57 -0
- twisk/templates/builtin/job.yaml +68 -0
- twisk/templates/builtin/pool.yaml +51 -0
- twisk/templates/builtin/service.yaml +73 -0
- twisk/templates/builtin/volume.yaml +40 -0
- twisk/templates/engine.py +351 -0
- twisk/templates/model.py +188 -0
- twisk/templates/registry.py +141 -0
- twisk/tui/__init__.py +0 -0
- twisk/tui/app.py +113 -0
- twisk/tui/format.py +63 -0
- twisk/tui/screens/__init__.py +0 -0
- twisk/tui/screens/detail.py +64 -0
- twisk/tui/screens/dialogs.py +200 -0
- twisk/tui/screens/editor.py +204 -0
- twisk/tui/screens/form.py +251 -0
- twisk/tui/screens/help.py +78 -0
- twisk/tui/screens/logs.py +129 -0
- twisk/tui/screens/main.py +1126 -0
- twisk/tui/twisk.tcss +255 -0
- twisk/tui/views.py +698 -0
- twisk/tui/widgets/__init__.py +0 -0
- twisk/tui/widgets/header.py +106 -0
- twisk/tui/widgets/vim.py +801 -0
- twisk-0.1.0b0.dist-info/METADATA +360 -0
- twisk-0.1.0b0.dist-info/RECORD +41 -0
- twisk-0.1.0b0.dist-info/WHEEL +4 -0
- twisk-0.1.0b0.dist-info/entry_points.txt +3 -0
- twisk-0.1.0b0.dist-info/licenses/LICENSE +199 -0
twisk/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from twisk.backend.base import (
|
|
2
|
+
Backend,
|
|
3
|
+
BackendError,
|
|
4
|
+
ClusterJobRow,
|
|
5
|
+
ClusterRow,
|
|
6
|
+
ClusterActivity,
|
|
7
|
+
InfraRow,
|
|
8
|
+
ManagedJobRow,
|
|
9
|
+
PoolRow,
|
|
10
|
+
ReplicaRow,
|
|
11
|
+
RequestRow,
|
|
12
|
+
ServerInfo,
|
|
13
|
+
ServerUnavailableError,
|
|
14
|
+
ServiceRow,
|
|
15
|
+
StorageRow,
|
|
16
|
+
UsageRow,
|
|
17
|
+
VolumeRow,
|
|
18
|
+
WorkspaceRow,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Backend",
|
|
23
|
+
"BackendError",
|
|
24
|
+
"ClusterJobRow",
|
|
25
|
+
"ClusterRow",
|
|
26
|
+
"ClusterActivity",
|
|
27
|
+
"InfraRow",
|
|
28
|
+
"ManagedJobRow",
|
|
29
|
+
"PoolRow",
|
|
30
|
+
"ReplicaRow",
|
|
31
|
+
"RequestRow",
|
|
32
|
+
"ServerInfo",
|
|
33
|
+
"ServerUnavailableError",
|
|
34
|
+
"ServiceRow",
|
|
35
|
+
"StorageRow",
|
|
36
|
+
"UsageRow",
|
|
37
|
+
"VolumeRow",
|
|
38
|
+
"WorkspaceRow",
|
|
39
|
+
]
|
twisk/backend/base.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Backend protocol and the plain row types the TUI renders.
|
|
2
|
+
|
|
3
|
+
The TUI only talks to a :class:`Backend`. :mod:`twisk.backend.skypilot`
|
|
4
|
+
implements it against a SkyPilot API server; :mod:`twisk.backend.fake` is an
|
|
5
|
+
in-memory simulation used by ``twisk --demo`` and the tests.
|
|
6
|
+
|
|
7
|
+
Long-running operations (launch, down, ...) return a SkyPilot request id
|
|
8
|
+
immediately; progress is followed with :meth:`Backend.stream_request_logs`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import threading
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, Callable, Protocol
|
|
16
|
+
|
|
17
|
+
from twisk.specs.models import ClusterSpec, JobSpec, PoolSpec, ServiceSpec, VolumeSpec
|
|
18
|
+
|
|
19
|
+
LogSink = Callable[[str], None]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BackendError(Exception):
|
|
23
|
+
"""Anything that went wrong talking to SkyPilot, with a user-facing message."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ServerUnavailableError(BackendError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ServerInfo:
|
|
32
|
+
endpoint: str
|
|
33
|
+
healthy: bool
|
|
34
|
+
version: str | None = None
|
|
35
|
+
api_version: str | None = None
|
|
36
|
+
user: str | None = None
|
|
37
|
+
error: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ClusterRow:
|
|
42
|
+
name: str
|
|
43
|
+
status: str
|
|
44
|
+
infra: str
|
|
45
|
+
resources: str
|
|
46
|
+
launched_at: float | None
|
|
47
|
+
autostop: int # minutes; -1 = off
|
|
48
|
+
to_down: bool
|
|
49
|
+
user: str
|
|
50
|
+
workspace: str = "default"
|
|
51
|
+
last_event: str | None = None
|
|
52
|
+
creation_yaml: str | None = None
|
|
53
|
+
details: dict[str, Any] = field(default_factory=dict, compare=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ClusterJobRow:
|
|
58
|
+
cluster: str
|
|
59
|
+
job_id: int
|
|
60
|
+
name: str
|
|
61
|
+
status: str
|
|
62
|
+
submitted_at: float | None
|
|
63
|
+
start_at: float | None
|
|
64
|
+
end_at: float | None
|
|
65
|
+
resources: str
|
|
66
|
+
user: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ManagedJobRow:
|
|
71
|
+
job_id: int
|
|
72
|
+
name: str
|
|
73
|
+
status: str
|
|
74
|
+
resources: str
|
|
75
|
+
infra: str
|
|
76
|
+
submitted_at: float | None
|
|
77
|
+
duration: float | None
|
|
78
|
+
recoveries: int
|
|
79
|
+
user: str
|
|
80
|
+
details: str | None = None
|
|
81
|
+
user_yaml: str | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class ReplicaRow:
|
|
86
|
+
service: str
|
|
87
|
+
replica_id: int
|
|
88
|
+
status: str
|
|
89
|
+
version: int | None
|
|
90
|
+
infra: str
|
|
91
|
+
resources: str
|
|
92
|
+
endpoint: str | None
|
|
93
|
+
launched_at: float | None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class ServiceRow:
|
|
98
|
+
name: str
|
|
99
|
+
status: str
|
|
100
|
+
endpoint: str | None
|
|
101
|
+
ready_replicas: int
|
|
102
|
+
total_replicas: int
|
|
103
|
+
versions: str
|
|
104
|
+
uptime: float | None # unix time the service became ready
|
|
105
|
+
replicas: tuple[ReplicaRow, ...] = ()
|
|
106
|
+
yaml: str | None = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class VolumeRow:
|
|
111
|
+
name: str
|
|
112
|
+
type: str
|
|
113
|
+
infra: str
|
|
114
|
+
size: str
|
|
115
|
+
status: str
|
|
116
|
+
used_by: tuple[str, ...]
|
|
117
|
+
launched_at: float | None
|
|
118
|
+
user: str
|
|
119
|
+
creation_yaml: str | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class RequestRow:
|
|
124
|
+
request_id: str
|
|
125
|
+
name: str
|
|
126
|
+
status: str
|
|
127
|
+
created_at: float | None
|
|
128
|
+
finished_at: float | None
|
|
129
|
+
cluster: str | None
|
|
130
|
+
user: str
|
|
131
|
+
status_msg: str | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class UsageRow:
|
|
136
|
+
"""GPU usage of one cluster (live or terminated) within a time window."""
|
|
137
|
+
|
|
138
|
+
name: str
|
|
139
|
+
status: str # live status, or "TERMINATED"
|
|
140
|
+
infra: str
|
|
141
|
+
resources: str
|
|
142
|
+
num_nodes: int
|
|
143
|
+
gpus: float # accelerators across all nodes
|
|
144
|
+
launched_at: float | None
|
|
145
|
+
duration: float # seconds the cluster was up within the window
|
|
146
|
+
user: str = "-"
|
|
147
|
+
workspace: str = "default"
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def gpu_hours(self) -> float:
|
|
151
|
+
return self.gpus * self.duration / 3600.0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Cluster-job statuses that mean the cluster is doing work.
|
|
155
|
+
ACTIVE_JOB_STATUSES = frozenset({"INIT", "PENDING", "SETTING_UP", "RUNNING", "STARTING"})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class ClusterActivity:
|
|
160
|
+
"""Whether an UP cluster is working, and if not, since when it's idle.
|
|
161
|
+
|
|
162
|
+
Mirrors SkyPilot's autostop notion of idleness: a cluster is busy while
|
|
163
|
+
any job is pending/running; otherwise it has been idle since its last
|
|
164
|
+
job ended (or since it was launched, if it never ran one). Interactive
|
|
165
|
+
SSH sessions are not visible to twisk and don't count.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
busy: bool
|
|
169
|
+
running_jobs: int
|
|
170
|
+
idle_since: float | None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def activity_from_jobs(launched_at: float | None, jobs: list["ClusterJobRow"]) -> ClusterActivity:
|
|
174
|
+
running = [j for j in jobs if j.status.upper() in ACTIVE_JOB_STATUSES]
|
|
175
|
+
if running:
|
|
176
|
+
return ClusterActivity(True, len(running), None)
|
|
177
|
+
ends = [j.end_at for j in jobs if j.end_at]
|
|
178
|
+
candidates = [t for t in (*ends, launched_at) if t]
|
|
179
|
+
return ClusterActivity(False, 0, max(candidates) if candidates else None)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class PoolRow:
|
|
184
|
+
name: str
|
|
185
|
+
status: str
|
|
186
|
+
ready_workers: int
|
|
187
|
+
total_workers: int
|
|
188
|
+
resources: str
|
|
189
|
+
workers: tuple["ReplicaRow", ...] = ()
|
|
190
|
+
yaml: str | None = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True)
|
|
194
|
+
class StorageRow:
|
|
195
|
+
name: str
|
|
196
|
+
stores: str # e.g. "S3, GCS"
|
|
197
|
+
status: str
|
|
198
|
+
launched_at: float | None
|
|
199
|
+
last_use: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class WorkspaceRow:
|
|
204
|
+
name: str
|
|
205
|
+
active: bool
|
|
206
|
+
writable: bool = True
|
|
207
|
+
detail: str = ""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True)
|
|
211
|
+
class InfraRow:
|
|
212
|
+
name: str
|
|
213
|
+
enabled: bool
|
|
214
|
+
detail: str = ""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Backend(Protocol):
|
|
218
|
+
"""Everything the TUI needs from SkyPilot."""
|
|
219
|
+
|
|
220
|
+
endpoint: str
|
|
221
|
+
|
|
222
|
+
def server_info(self) -> ServerInfo: ...
|
|
223
|
+
|
|
224
|
+
# Whose resources the list_* calls return: only the caller's (False) or
|
|
225
|
+
# every user's (True). Toggled from the TUI.
|
|
226
|
+
all_users: bool
|
|
227
|
+
|
|
228
|
+
# listing
|
|
229
|
+
def list_clusters(self, refresh: bool = False) -> list[ClusterRow]: ...
|
|
230
|
+
def list_cluster_jobs(self, cluster: str) -> list[ClusterJobRow]: ...
|
|
231
|
+
def list_managed_jobs(self) -> list[ManagedJobRow]: ...
|
|
232
|
+
def list_usage(self, days: int | None = 30) -> list[UsageRow]: ...
|
|
233
|
+
def cluster_activity(self, clusters: list[ClusterRow]) -> dict[str, ClusterActivity]: ...
|
|
234
|
+
def list_pools(self) -> list[PoolRow]: ...
|
|
235
|
+
def list_storage(self) -> list[StorageRow]: ...
|
|
236
|
+
def list_workspaces(self) -> list[WorkspaceRow]: ...
|
|
237
|
+
def list_services(self) -> list[ServiceRow]: ...
|
|
238
|
+
def list_volumes(self) -> list[VolumeRow]: ...
|
|
239
|
+
def list_requests(self, limit: int = 100) -> list[RequestRow]: ...
|
|
240
|
+
def list_infra(self) -> list[InfraRow]: ...
|
|
241
|
+
|
|
242
|
+
# clusters
|
|
243
|
+
def launch_cluster(self, spec: ClusterSpec) -> str: ...
|
|
244
|
+
def exec_on_cluster(self, cluster: str, command: str) -> str: ...
|
|
245
|
+
def stop_cluster(self, cluster: str) -> str: ...
|
|
246
|
+
def start_cluster(self, cluster: str) -> str: ...
|
|
247
|
+
def down_cluster(self, cluster: str) -> str: ...
|
|
248
|
+
def set_autostop(self, cluster: str, minutes: int, down: bool) -> str: ...
|
|
249
|
+
def cancel_cluster_job(self, cluster: str, job_id: int) -> str: ...
|
|
250
|
+
def ssh_argv(self, cluster: str) -> list[str]: ...
|
|
251
|
+
|
|
252
|
+
# workspaces
|
|
253
|
+
def set_workspace(self, name: str) -> None: ...
|
|
254
|
+
|
|
255
|
+
# managed jobs (spec.pool, if set, submits the job to that pool)
|
|
256
|
+
def launch_managed_job(self, spec: JobSpec) -> str: ...
|
|
257
|
+
def cancel_managed_job(self, job_id: int) -> str: ...
|
|
258
|
+
|
|
259
|
+
# services
|
|
260
|
+
def serve_up(self, spec: ServiceSpec) -> str: ...
|
|
261
|
+
def serve_update(self, spec: ServiceSpec) -> str: ...
|
|
262
|
+
def serve_down(self, name: str) -> str: ...
|
|
263
|
+
def terminate_replica(self, service: str, replica_id: int) -> str: ...
|
|
264
|
+
|
|
265
|
+
# pools
|
|
266
|
+
def pool_apply(self, spec: PoolSpec) -> str: ...
|
|
267
|
+
def pool_down(self, name: str) -> str: ...
|
|
268
|
+
|
|
269
|
+
# storage buckets
|
|
270
|
+
def delete_storage(self, name: str) -> str: ...
|
|
271
|
+
|
|
272
|
+
# volumes
|
|
273
|
+
def apply_volume(self, spec: VolumeSpec) -> str: ...
|
|
274
|
+
def delete_volume(self, name: str) -> str: ...
|
|
275
|
+
|
|
276
|
+
# requests
|
|
277
|
+
def cancel_request(self, request_id: str) -> None: ...
|
|
278
|
+
|
|
279
|
+
# logs: block until done or `stop` is set, feeding lines to `sink`
|
|
280
|
+
def stream_request_logs(self, request_id: str, sink: LogSink, stop: threading.Event) -> None: ...
|
|
281
|
+
def stream_cluster_job_logs(self, cluster: str, job_id: int | None, sink: LogSink, stop: threading.Event) -> None: ...
|
|
282
|
+
def stream_managed_job_logs(self, job_id: int, sink: LogSink, stop: threading.Event, controller: bool = False, follow: bool = True) -> None: ...
|
|
283
|
+
def stream_service_logs(self, service: str, target: str, replica_id: int | None, sink: LogSink, stop: threading.Event) -> None: ...
|
|
284
|
+
def stream_pool_logs(self, pool: str, target: str, worker_id: int | None, sink: LogSink, stop: threading.Event) -> None: ...
|