hyperliquid-cli-python 0.1.0__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.
- hl_cli/__init__.py +1 -0
- hl_cli/cli/__init__.py +1 -0
- hl_cli/cli/argparse_main.py +814 -0
- hl_cli/cli/markets_tui.py +399 -0
- hl_cli/cli/runtime.py +82 -0
- hl_cli/commands/__init__.py +1 -0
- hl_cli/commands/app.py +1081 -0
- hl_cli/commands/order.py +918 -0
- hl_cli/core/__init__.py +1 -0
- hl_cli/core/context.py +156 -0
- hl_cli/core/order_config.py +23 -0
- hl_cli/infra/__init__.py +1 -0
- hl_cli/infra/db.py +277 -0
- hl_cli/infra/paths.py +5 -0
- hl_cli/utils/__init__.py +1 -0
- hl_cli/utils/market_table.py +66 -0
- hl_cli/utils/output.py +476 -0
- hl_cli/utils/validators.py +45 -0
- hl_cli/utils/watch.py +28 -0
- hyperliquid_cli_python-0.1.0.dist-info/METADATA +269 -0
- hyperliquid_cli_python-0.1.0.dist-info/RECORD +25 -0
- hyperliquid_cli_python-0.1.0.dist-info/WHEEL +5 -0
- hyperliquid_cli_python-0.1.0.dist-info/entry_points.txt +2 -0
- hyperliquid_cli_python-0.1.0.dist-info/licenses/LICENSE +32 -0
- hyperliquid_cli_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import select
|
|
4
|
+
import sys
|
|
5
|
+
import termios
|
|
6
|
+
import time
|
|
7
|
+
import tty
|
|
8
|
+
from itertools import cycle
|
|
9
|
+
from typing import Any, Callable, Literal, Optional
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.live import Live
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
|
|
15
|
+
from ..utils.market_table import (
|
|
16
|
+
build_market_table,
|
|
17
|
+
market_table_columns,
|
|
18
|
+
market_table_row_values,
|
|
19
|
+
market_table_widths,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MarketsRows = dict[str, list[dict[str, Any]]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _market_row_dex(row: dict[str, Any]) -> str:
|
|
27
|
+
if row.get("marketType") == "spot":
|
|
28
|
+
return ""
|
|
29
|
+
coin = str(row.get("coin", ""))
|
|
30
|
+
if ":" in coin:
|
|
31
|
+
return coin.split(":", 1)[0]
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _market_row_kind(row: dict[str, Any]) -> Literal["perp", "spot"]:
|
|
36
|
+
return "spot" if row.get("marketType") == "spot" else "perp"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MarketsTuiState:
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
self.scope: Literal["all", "perp", "spot"] = "all"
|
|
42
|
+
self.selected = 0
|
|
43
|
+
self.scroll = 0
|
|
44
|
+
self.mode: Literal["normal", "search"] = "normal"
|
|
45
|
+
self.search_direction: Literal["forward", "backward"] = "forward"
|
|
46
|
+
self.search_query = ""
|
|
47
|
+
self.search_buffer = ""
|
|
48
|
+
|
|
49
|
+
def rows(self, rows: MarketsRows) -> list[dict[str, Any]]:
|
|
50
|
+
merged = [*rows["perpMarkets"], *rows["spotMarkets"]]
|
|
51
|
+
if self.scope == "all":
|
|
52
|
+
return merged
|
|
53
|
+
return [row for row in merged if _market_row_kind(row) == self.scope]
|
|
54
|
+
|
|
55
|
+
def clamp(self, total: int, window_size: int) -> None:
|
|
56
|
+
if total <= 0:
|
|
57
|
+
self.selected = 0
|
|
58
|
+
self.scroll = 0
|
|
59
|
+
return
|
|
60
|
+
self.selected = max(0, min(self.selected, total - 1))
|
|
61
|
+
max_scroll = max(0, total - window_size)
|
|
62
|
+
if self.selected < self.scroll:
|
|
63
|
+
self.scroll = self.selected
|
|
64
|
+
elif self.selected >= self.scroll + window_size:
|
|
65
|
+
self.scroll = self.selected - window_size + 1
|
|
66
|
+
self.scroll = max(0, min(self.scroll, max_scroll))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@contextlib.contextmanager
|
|
70
|
+
def _raw_tty_mode() -> Any:
|
|
71
|
+
if not sys.stdin.isatty():
|
|
72
|
+
yield False
|
|
73
|
+
return
|
|
74
|
+
fd = sys.stdin.fileno()
|
|
75
|
+
old = termios.tcgetattr(fd)
|
|
76
|
+
try:
|
|
77
|
+
tty.setcbreak(fd)
|
|
78
|
+
yield True
|
|
79
|
+
finally:
|
|
80
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _read_key(timeout: float = 0.0) -> Optional[str]:
|
|
84
|
+
if not sys.stdin.isatty():
|
|
85
|
+
return None
|
|
86
|
+
ready, _, _ = select.select([sys.stdin], [], [], timeout)
|
|
87
|
+
if not ready:
|
|
88
|
+
return None
|
|
89
|
+
first = sys.stdin.read(1)
|
|
90
|
+
if first != "\x1b":
|
|
91
|
+
return first
|
|
92
|
+
ready, _, _ = select.select([sys.stdin], [], [], 0.01)
|
|
93
|
+
if not ready:
|
|
94
|
+
return first
|
|
95
|
+
second = sys.stdin.read(1)
|
|
96
|
+
if second != "[":
|
|
97
|
+
return first + second
|
|
98
|
+
ready, _, _ = select.select([sys.stdin], [], [], 0.01)
|
|
99
|
+
if not ready:
|
|
100
|
+
return first + second
|
|
101
|
+
third = sys.stdin.read(1)
|
|
102
|
+
return first + second + third
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _matches_query(row: dict[str, Any], query: str) -> bool:
|
|
106
|
+
needle = query.strip().lower()
|
|
107
|
+
if not needle:
|
|
108
|
+
return False
|
|
109
|
+
haystacks = [
|
|
110
|
+
str(row.get("coin", "")),
|
|
111
|
+
str(row.get("pairName", "")),
|
|
112
|
+
str(row.get("category", "")),
|
|
113
|
+
]
|
|
114
|
+
return any(needle in value.lower() for value in haystacks)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _find_match(rows: list[dict[str, Any]], start: int, query: str, *, forward: bool) -> Optional[int]:
|
|
118
|
+
if not rows or not query.strip():
|
|
119
|
+
return None
|
|
120
|
+
total = len(rows)
|
|
121
|
+
step = 1 if forward else -1
|
|
122
|
+
index = start
|
|
123
|
+
for _ in range(total):
|
|
124
|
+
index = (index + step) % total
|
|
125
|
+
if _matches_query(rows[index], query):
|
|
126
|
+
return index
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _jump_to_match(
|
|
131
|
+
state: MarketsTuiState,
|
|
132
|
+
rows: MarketsRows,
|
|
133
|
+
*,
|
|
134
|
+
forward: bool,
|
|
135
|
+
wrap_from_current: bool,
|
|
136
|
+
window_size: int,
|
|
137
|
+
) -> None:
|
|
138
|
+
current_rows = state.rows(rows)
|
|
139
|
+
if not current_rows or not state.search_query.strip():
|
|
140
|
+
return
|
|
141
|
+
start = state.selected - 1 if forward and not wrap_from_current else state.selected
|
|
142
|
+
if not forward and not wrap_from_current:
|
|
143
|
+
start = state.selected + 1
|
|
144
|
+
match = _find_match(current_rows, start, state.search_query, forward=forward)
|
|
145
|
+
if match is None:
|
|
146
|
+
return
|
|
147
|
+
state.selected = match
|
|
148
|
+
state.clamp(len(current_rows), window_size)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _handle_key(key: Optional[str], state: MarketsTuiState, rows: MarketsRows, window_size: int) -> bool:
|
|
152
|
+
current_rows = state.rows(rows)
|
|
153
|
+
total = len(current_rows)
|
|
154
|
+
if state.mode == "search":
|
|
155
|
+
if key is None:
|
|
156
|
+
return False
|
|
157
|
+
if key in {"\r", "\n"}:
|
|
158
|
+
state.mode = "normal"
|
|
159
|
+
state.search_query = state.search_buffer
|
|
160
|
+
_jump_to_match(
|
|
161
|
+
state,
|
|
162
|
+
rows,
|
|
163
|
+
forward=state.search_direction == "forward",
|
|
164
|
+
wrap_from_current=False,
|
|
165
|
+
window_size=window_size,
|
|
166
|
+
)
|
|
167
|
+
return False
|
|
168
|
+
if key == "\x1b":
|
|
169
|
+
state.mode = "normal"
|
|
170
|
+
state.search_buffer = state.search_query
|
|
171
|
+
return False
|
|
172
|
+
if key in {"\x7f", "\b"}:
|
|
173
|
+
state.search_buffer = state.search_buffer[:-1]
|
|
174
|
+
return False
|
|
175
|
+
if len(key) == 1 and key.isprintable():
|
|
176
|
+
state.search_buffer += key
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
if key in {"q", "\x03"}:
|
|
180
|
+
return True
|
|
181
|
+
if key in {"/", "?"}:
|
|
182
|
+
state.mode = "search"
|
|
183
|
+
state.search_direction = "forward" if key == "/" else "backward"
|
|
184
|
+
state.search_buffer = state.search_query
|
|
185
|
+
return False
|
|
186
|
+
if key in {"j", "\x1b[B"}:
|
|
187
|
+
state.selected += 1
|
|
188
|
+
elif key in {"k", "\x1b[A"}:
|
|
189
|
+
state.selected -= 1
|
|
190
|
+
elif key == "J":
|
|
191
|
+
state.selected += 10
|
|
192
|
+
elif key == "K":
|
|
193
|
+
state.selected -= 10
|
|
194
|
+
elif key == "g":
|
|
195
|
+
state.selected = 0
|
|
196
|
+
elif key == "G":
|
|
197
|
+
state.selected = max(0, total - 1)
|
|
198
|
+
elif key == "n":
|
|
199
|
+
_jump_to_match(
|
|
200
|
+
state,
|
|
201
|
+
rows,
|
|
202
|
+
forward=state.search_direction == "forward",
|
|
203
|
+
wrap_from_current=True,
|
|
204
|
+
window_size=window_size,
|
|
205
|
+
)
|
|
206
|
+
return False
|
|
207
|
+
elif key == "N":
|
|
208
|
+
_jump_to_match(
|
|
209
|
+
state,
|
|
210
|
+
rows,
|
|
211
|
+
forward=state.search_direction != "forward",
|
|
212
|
+
wrap_from_current=True,
|
|
213
|
+
window_size=window_size,
|
|
214
|
+
)
|
|
215
|
+
return False
|
|
216
|
+
elif key == "h":
|
|
217
|
+
state.scope = "perp"
|
|
218
|
+
state.selected = 0
|
|
219
|
+
state.scroll = 0
|
|
220
|
+
elif key == "l":
|
|
221
|
+
state.scope = "spot"
|
|
222
|
+
state.selected = 0
|
|
223
|
+
state.scroll = 0
|
|
224
|
+
elif key == "a":
|
|
225
|
+
state.scope = "all"
|
|
226
|
+
state.selected = 0
|
|
227
|
+
state.scroll = 0
|
|
228
|
+
state.clamp(len(state.rows(rows)), window_size)
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _render_table(
|
|
233
|
+
rows: MarketsRows,
|
|
234
|
+
include_category: bool,
|
|
235
|
+
*,
|
|
236
|
+
console: Console,
|
|
237
|
+
state: MarketsTuiState,
|
|
238
|
+
widths_by_scope: dict[str, list[int]],
|
|
239
|
+
format_price: Callable[[Any], str],
|
|
240
|
+
format_usd: Callable[[Any], str],
|
|
241
|
+
format_rate_pct: Callable[[Any], str],
|
|
242
|
+
) -> Panel:
|
|
243
|
+
current_rows = state.rows(rows)
|
|
244
|
+
# Leave room for the panel border, title/subtitle, and terminal prompt line.
|
|
245
|
+
window_size = max(5, console.size.height - 8)
|
|
246
|
+
state.clamp(len(current_rows), window_size)
|
|
247
|
+
visible_rows = current_rows[state.scroll : state.scroll + window_size]
|
|
248
|
+
selected_index = state.selected - state.scroll
|
|
249
|
+
|
|
250
|
+
show_perp_only_fields = state.scope != "spot"
|
|
251
|
+
columns = market_table_columns(
|
|
252
|
+
include_category=include_category,
|
|
253
|
+
show_perp_only_fields=show_perp_only_fields,
|
|
254
|
+
)
|
|
255
|
+
rendered_rows = [
|
|
256
|
+
market_table_row_values(
|
|
257
|
+
row,
|
|
258
|
+
include_category=include_category,
|
|
259
|
+
show_perp_only_fields=show_perp_only_fields,
|
|
260
|
+
format_price=format_price,
|
|
261
|
+
format_usd=format_usd,
|
|
262
|
+
format_rate_pct=format_rate_pct,
|
|
263
|
+
)
|
|
264
|
+
for row in visible_rows
|
|
265
|
+
]
|
|
266
|
+
table = build_market_table(
|
|
267
|
+
title=f"Markets ({len(rows['perpMarkets'])} perps, {len(rows['spotMarkets'])} spot)",
|
|
268
|
+
columns=columns,
|
|
269
|
+
rendered_rows=rendered_rows,
|
|
270
|
+
widths=widths_by_scope[state.scope],
|
|
271
|
+
highlighted_index=selected_index,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if state.mode == "search":
|
|
275
|
+
prefix = "/" if state.search_direction == "forward" else "?"
|
|
276
|
+
help_text = f"{prefix}{state.search_buffer}"
|
|
277
|
+
else:
|
|
278
|
+
help_text = (
|
|
279
|
+
f"scope={state.scope} rows={len(current_rows)} "
|
|
280
|
+
f"search={state.search_query or '-'} "
|
|
281
|
+
"hjkl/arrows move gg/G top/bottom / ? search n/N next/prev h perp l spot a all q quit"
|
|
282
|
+
)
|
|
283
|
+
return Panel(table, subtitle=help_text)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def run_markets_tui(
|
|
287
|
+
*,
|
|
288
|
+
console: Console,
|
|
289
|
+
rows: MarketsRows,
|
|
290
|
+
include_category: bool,
|
|
291
|
+
next_mids: Callable[[str], dict[str, Any]],
|
|
292
|
+
sort_rows: Callable[[MarketsRows], MarketsRows],
|
|
293
|
+
prepare_output: Callable[[MarketsRows], MarketsRows],
|
|
294
|
+
format_price: Callable[[Any], str],
|
|
295
|
+
format_usd: Callable[[Any], str],
|
|
296
|
+
format_rate_pct: Callable[[Any], str],
|
|
297
|
+
as_json: bool,
|
|
298
|
+
) -> None:
|
|
299
|
+
if as_json:
|
|
300
|
+
print(json.dumps(prepare_output(rows), ensure_ascii=False))
|
|
301
|
+
|
|
302
|
+
row_map = {row["coin"]: row for row in [*rows["perpMarkets"], *rows["spotMarkets"]]}
|
|
303
|
+
dexes = sorted({_market_row_dex(row) for row in row_map.values()}, key=lambda value: (value != "", value))
|
|
304
|
+
if not dexes:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
def refresh_rows() -> MarketsRows:
|
|
308
|
+
return sort_rows(rows)
|
|
309
|
+
|
|
310
|
+
if as_json:
|
|
311
|
+
try:
|
|
312
|
+
for dex in cycle(dexes):
|
|
313
|
+
mids = next_mids(dex)
|
|
314
|
+
for coin, row in row_map.items():
|
|
315
|
+
if _market_row_dex(row) != dex:
|
|
316
|
+
continue
|
|
317
|
+
if coin in mids:
|
|
318
|
+
row["price"] = mids[coin]
|
|
319
|
+
print(json.dumps(prepare_output(refresh_rows()), ensure_ascii=False))
|
|
320
|
+
time.sleep(1.0)
|
|
321
|
+
except KeyboardInterrupt:
|
|
322
|
+
return
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
state = MarketsTuiState()
|
|
326
|
+
widths_by_scope: dict[str, list[int]] = {}
|
|
327
|
+
for scope in ("all", "perp", "spot"):
|
|
328
|
+
state.scope = scope
|
|
329
|
+
scope_rows = state.rows(rows)
|
|
330
|
+
show_perp_only_fields = scope != "spot"
|
|
331
|
+
columns = market_table_columns(
|
|
332
|
+
include_category=include_category,
|
|
333
|
+
show_perp_only_fields=show_perp_only_fields,
|
|
334
|
+
)
|
|
335
|
+
rendered_rows = [
|
|
336
|
+
market_table_row_values(
|
|
337
|
+
row,
|
|
338
|
+
include_category=include_category,
|
|
339
|
+
show_perp_only_fields=show_perp_only_fields,
|
|
340
|
+
format_price=format_price,
|
|
341
|
+
format_usd=format_usd,
|
|
342
|
+
format_rate_pct=format_rate_pct,
|
|
343
|
+
)
|
|
344
|
+
for row in scope_rows
|
|
345
|
+
]
|
|
346
|
+
widths_by_scope[scope] = market_table_widths(columns, rendered_rows)
|
|
347
|
+
state.scope = "all"
|
|
348
|
+
try:
|
|
349
|
+
with _raw_tty_mode():
|
|
350
|
+
initial = _render_table(
|
|
351
|
+
rows,
|
|
352
|
+
include_category,
|
|
353
|
+
console=console,
|
|
354
|
+
state=state,
|
|
355
|
+
widths_by_scope=widths_by_scope,
|
|
356
|
+
format_price=format_price,
|
|
357
|
+
format_usd=format_usd,
|
|
358
|
+
format_rate_pct=format_rate_pct,
|
|
359
|
+
)
|
|
360
|
+
with Live(initial, console=console, refresh_per_second=8, screen=True) as live:
|
|
361
|
+
for dex in cycle(dexes):
|
|
362
|
+
if _handle_key(_read_key(0.0), state, rows, 24):
|
|
363
|
+
return
|
|
364
|
+
mids = next_mids(dex)
|
|
365
|
+
for coin, row in row_map.items():
|
|
366
|
+
if _market_row_dex(row) != dex:
|
|
367
|
+
continue
|
|
368
|
+
if coin in mids:
|
|
369
|
+
row["price"] = mids[coin]
|
|
370
|
+
live.update(
|
|
371
|
+
_render_table(
|
|
372
|
+
refresh_rows(),
|
|
373
|
+
include_category,
|
|
374
|
+
console=console,
|
|
375
|
+
state=state,
|
|
376
|
+
widths_by_scope=widths_by_scope,
|
|
377
|
+
format_price=format_price,
|
|
378
|
+
format_usd=format_usd,
|
|
379
|
+
format_rate_pct=format_rate_pct,
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
tick_end = time.time() + 1.0
|
|
383
|
+
while time.time() < tick_end:
|
|
384
|
+
if _handle_key(_read_key(0.1), state, rows, 24):
|
|
385
|
+
return
|
|
386
|
+
live.update(
|
|
387
|
+
_render_table(
|
|
388
|
+
refresh_rows(),
|
|
389
|
+
include_category,
|
|
390
|
+
console=console,
|
|
391
|
+
state=state,
|
|
392
|
+
widths_by_scope=widths_by_scope,
|
|
393
|
+
format_price=format_price,
|
|
394
|
+
format_usd=format_usd,
|
|
395
|
+
format_rate_pct=format_rate_pct,
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
except KeyboardInterrupt:
|
|
399
|
+
return
|
hl_cli/cli/runtime.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import Future
|
|
3
|
+
from functools import wraps
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Awaitable, Callable, ParamSpec, TypeVar
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ..core.context import CLIContext
|
|
12
|
+
from ..utils.output import out_error
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
P = ParamSpec("P")
|
|
16
|
+
R = TypeVar("R")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cli_context(ctx: Any) -> CLIContext:
|
|
20
|
+
return ctx.obj["context"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def json_output_enabled(ctx: Any) -> bool:
|
|
24
|
+
return bool(ctx.obj["json"])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def finish_command(ctx: Any) -> None:
|
|
28
|
+
if not json_output_enabled(ctx):
|
|
29
|
+
elapsed = time.perf_counter() - float(ctx.obj["start"])
|
|
30
|
+
print(f"\nExecution time: {elapsed:.2f}s")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def confirm(message: str, default: bool = False) -> bool:
|
|
34
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
35
|
+
answer = input(f"{message} {suffix}: ").strip().lower()
|
|
36
|
+
if not answer:
|
|
37
|
+
return default
|
|
38
|
+
return answer in {"y", "yes"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render_table(title: str, columns: list[str], rows: list[list[Any]]) -> None:
|
|
42
|
+
table = Table(title=title)
|
|
43
|
+
for c in columns:
|
|
44
|
+
table.add_column(c)
|
|
45
|
+
for row in rows:
|
|
46
|
+
table.add_row(*[str(v) for v in row])
|
|
47
|
+
console.print(table)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_blocking(coro: Awaitable[R]) -> R:
|
|
51
|
+
try:
|
|
52
|
+
asyncio.get_running_loop()
|
|
53
|
+
except RuntimeError:
|
|
54
|
+
return asyncio.run(coro)
|
|
55
|
+
|
|
56
|
+
result: Future[R] = Future()
|
|
57
|
+
|
|
58
|
+
def runner() -> None:
|
|
59
|
+
try:
|
|
60
|
+
value = asyncio.run(coro)
|
|
61
|
+
except BaseException as exc: # noqa: BLE001
|
|
62
|
+
result.set_exception(exc)
|
|
63
|
+
return
|
|
64
|
+
result.set_result(value)
|
|
65
|
+
|
|
66
|
+
thread = threading.Thread(target=runner, daemon=True)
|
|
67
|
+
thread.start()
|
|
68
|
+
return result.result()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cli_command(fn: Callable[P, R]) -> Callable[P, R]:
|
|
72
|
+
@wraps(fn)
|
|
73
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
74
|
+
try:
|
|
75
|
+
return fn(*args, **kwargs)
|
|
76
|
+
except SystemExit:
|
|
77
|
+
raise
|
|
78
|
+
except Exception as exc: # noqa: BLE001
|
|
79
|
+
out_error(str(exc))
|
|
80
|
+
raise SystemExit(1) from exc
|
|
81
|
+
|
|
82
|
+
return wrapper
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command implementations."""
|