-
Notifications
You must be signed in to change notification settings - Fork 17
/
preview.py
184 lines (155 loc) · 5.85 KB
/
preview.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import os
import sys
import termios
import tty
import webbrowser
from pathlib import Path
from typing import List
from flytekit.core.workflow import PythonFunctionWorkflow
from google.protobuf.json_format import MessageToJson
from latch_sdk_config.latch import config
import latch_cli.menus as menus
from latch_cli.centromere.utils import _import_flyte_objects
from latch_cli.tinyrequests import post
from latch_cli.utils import current_workspace, retrieve_or_login
# TODO(ayush): make this import the `wf` directory and use the package root
# instead of the workflow name. also redo the frontend, also make it open the
# page
def preview(pkg_root: Path):
"""Generate a preview of the parameter interface for a workflow.
This will allow a user to see how their parameter interface will look
without having to first register their workflow.
Args:
pkg_root: A valid path pointing to the worklow code a user wishes to
preview. The path can be absolute or relative.
Example:
>>> preview("wf.__init__.alphafold_wf")
"""
try:
modules = _import_flyte_objects([pkg_root.resolve()])
wfs: dict[str, PythonFunctionWorkflow] = {}
for module in modules:
for flyte_obj in module.__dict__.values():
if isinstance(flyte_obj, PythonFunctionWorkflow):
wfs[flyte_obj.name] = flyte_obj
if len(wfs) == 0:
raise ValueError(f"Unable to find a workflow definition in {pkg_root}")
except ImportError as e:
raise ValueError(
f"Unable to find {e.name} - make sure that all necessary packages"
" are installed and you have the correct function name."
)
wf = list(wfs.values())[0]
if len(wfs) > 1:
wf = wfs[
_select_workflow_tui(
title="Select which workflow to preview",
options=list(wfs.keys()),
)
]
resp = post(
url=config.api.workflow.preview,
headers={"Authorization": f"Bearer {retrieve_or_login()}"},
json={
"workflow_ui_preview": MessageToJson(wf.interface.to_flyte_idl().inputs),
"ws_account_id": current_workspace(),
},
)
resp.raise_for_status()
url = f"{config.console_url}/preview/parameters"
webbrowser.open(url)
# TODO(ayush): abstract this logic in a unified interface that all tui commands use
def _select_workflow_tui(title: str, options: List[str], clear_terminal: bool = True):
"""
Renders a terminal UI that allows users to select one of the options
listed in `options`
Args:
title: The title of the selection window.
options: A list of names for each of the options.
clear_terminal: Whether or not to clear the entire terminal window
before displaying - default False
"""
if len(options) == 0:
raise ValueError("No options given")
def render(
curr_selected: int,
start_index: int = 0,
max_per_page: int = 10,
indent: str = " ",
) -> int:
if curr_selected < 0 or curr_selected >= len(options):
curr_selected = 0
menus._print(title)
menus.line_down(2)
num_lines_rendered = 4 # 4 "extra" lines for header + footer
for i in range(start_index, start_index + max_per_page):
if i >= len(options):
break
name = options[i]
if i == curr_selected:
color = "\x1b[38;5;40m"
bold = "\x1b[1m"
reset = "\x1b[0m"
menus._print(f"{indent}{color}{bold}{name}{reset}\x1b[1E")
else:
menus._print(f"{indent}{name}\x1b[1E")
num_lines_rendered += 1
menus.line_down(1)
control_str = "[ARROW-KEYS] Navigate\t[ENTER] Select\t[Q] Quit"
menus._print(control_str)
menus.line_up(num_lines_rendered - 1)
menus._show()
return num_lines_rendered
old_settings = termios.tcgetattr(sys.stdin.fileno())
tty.setraw(sys.stdin.fileno())
curr_selected = 0
start_index = 0
_, term_height = os.get_terminal_size()
menus.remove_cursor()
if not clear_terminal:
_, curs_height = menus.current_cursor_position()
max_per_page = term_height - curs_height - 4
else:
menus.clear_screen()
menus.move_cursor((0, 0))
max_per_page = term_height - 4
num_lines_rendered = render(
curr_selected,
start_index=start_index,
max_per_page=max_per_page,
)
try:
while True:
b = menus.read_bytes(1)
if b == b"\r":
return options[curr_selected]
elif b == b"\x1b":
b = menus.read_bytes(2)
if b == b"[A": # Up Arrow
curr_selected = max(curr_selected - 1, 0)
if (
curr_selected - start_index < max_per_page // 2
and start_index > 0
):
start_index -= 1
elif b == b"[B": # Down Arrow
curr_selected = min(curr_selected + 1, len(options) - 1)
if (
curr_selected - start_index > max_per_page // 2
and start_index < len(options) - max_per_page
):
start_index += 1
else:
continue
menus.clear(num_lines_rendered)
num_lines_rendered = render(
curr_selected,
start_index=start_index,
max_per_page=max_per_page,
)
except KeyboardInterrupt: ...
finally:
menus.clear(num_lines_rendered)
menus.reveal_cursor()
menus._show()
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings)