openbb-cli 1.0.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.
- openbb_cli/__init__.py +1 -0
- openbb_cli/argparse_translator/__init__.py +0 -0
- openbb_cli/argparse_translator/argparse_class_processor.py +148 -0
- openbb_cli/argparse_translator/argparse_translator.py +627 -0
- openbb_cli/argparse_translator/obbject_registry.py +94 -0
- openbb_cli/assets/routines/routine_example.openbb +21 -0
- openbb_cli/assets/styles/default/Consolas.ttf +0 -0
- openbb_cli/assets/styles/default/dark.mpfstyle.json +47 -0
- openbb_cli/assets/styles/default/dark.mplrc.json +7 -0
- openbb_cli/assets/styles/default/dark.mplstyle +96 -0
- openbb_cli/assets/styles/default/dark.pltstyle.json +132 -0
- openbb_cli/assets/styles/default/dark.richstyle.json +9 -0
- openbb_cli/assets/styles/default/light.mpfstyle.json +47 -0
- openbb_cli/assets/styles/default/light.mplrc.json +7 -0
- openbb_cli/assets/styles/default/light.mplstyle +95 -0
- openbb_cli/assets/styles/default/light.pltstyle.json +871 -0
- openbb_cli/assets/styles/default/light.richstyle.json +9 -0
- openbb_cli/assets/styles/default/tables.pltstyle.json +102 -0
- openbb_cli/assets/styles/user/openbb.richstyle.json +9 -0
- openbb_cli/cli.py +20 -0
- openbb_cli/config/__init__.py +1 -0
- openbb_cli/config/completer.py +427 -0
- openbb_cli/config/console.py +93 -0
- openbb_cli/config/constants.py +80 -0
- openbb_cli/config/menu_text.py +165 -0
- openbb_cli/config/setup.py +11 -0
- openbb_cli/config/style.py +108 -0
- openbb_cli/controllers/base_controller.py +941 -0
- openbb_cli/controllers/base_platform_controller.py +349 -0
- openbb_cli/controllers/choices.py +324 -0
- openbb_cli/controllers/cli_controller.py +946 -0
- openbb_cli/controllers/hub_service.py +107 -0
- openbb_cli/controllers/platform_controller_factory.py +58 -0
- openbb_cli/controllers/script_parser.py +488 -0
- openbb_cli/controllers/settings_controller.py +373 -0
- openbb_cli/controllers/utils.py +967 -0
- openbb_cli/models/settings.py +73 -0
- openbb_cli/session.py +92 -0
- openbb_cli-1.0.0.dist-info/METADATA +91 -0
- openbb_cli-1.0.0.dist-info/RECORD +42 -0
- openbb_cli-1.0.0.dist-info/WHEEL +4 -0
- openbb_cli-1.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
"""Base controller for the CLI."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import difflib
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from abc import ABCMeta, abstractmethod
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from openbb_cli.config.completer import NestedCompleter
|
|
14
|
+
from openbb_cli.config.constants import SCRIPT_TAGS
|
|
15
|
+
from openbb_cli.controllers.choices import build_controller_choice_map
|
|
16
|
+
from openbb_cli.controllers.hub_service import upload_routine
|
|
17
|
+
from openbb_cli.controllers.utils import (
|
|
18
|
+
check_file_type_saved,
|
|
19
|
+
check_positive,
|
|
20
|
+
get_flair_and_username,
|
|
21
|
+
parse_and_split_input,
|
|
22
|
+
print_guest_block_msg,
|
|
23
|
+
print_rich_table,
|
|
24
|
+
remove_file,
|
|
25
|
+
system_clear,
|
|
26
|
+
)
|
|
27
|
+
from openbb_cli.session import Session
|
|
28
|
+
from prompt_toolkit.formatted_text import HTML
|
|
29
|
+
from prompt_toolkit.styles import Style
|
|
30
|
+
|
|
31
|
+
# pylint: disable=C0301,C0302,R0902,global-statement,too-many-boolean-expressions
|
|
32
|
+
# pylint: disable=R0912
|
|
33
|
+
|
|
34
|
+
controllers: Dict[str, Any] = {}
|
|
35
|
+
session = Session()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# TODO: We should try to avoid these global variables
|
|
39
|
+
RECORD_SESSION = False
|
|
40
|
+
RECORD_SESSION_LOCAL_ONLY = False
|
|
41
|
+
SESSION_RECORDED = list()
|
|
42
|
+
SESSION_RECORDED_NAME = ""
|
|
43
|
+
SESSION_RECORDED_DESCRIPTION = ""
|
|
44
|
+
SESSION_RECORDED_TAGS = ""
|
|
45
|
+
SESSION_RECORDED_PUBLIC = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseController(metaclass=ABCMeta):
|
|
49
|
+
"""Base class for a cli controller."""
|
|
50
|
+
|
|
51
|
+
CHOICES_COMMON = [
|
|
52
|
+
"cls",
|
|
53
|
+
"home",
|
|
54
|
+
"h",
|
|
55
|
+
"?",
|
|
56
|
+
"help",
|
|
57
|
+
"q",
|
|
58
|
+
"quit",
|
|
59
|
+
"..",
|
|
60
|
+
"e",
|
|
61
|
+
"exit",
|
|
62
|
+
"r",
|
|
63
|
+
"reset",
|
|
64
|
+
"stop",
|
|
65
|
+
"whoami",
|
|
66
|
+
"results",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
CHOICES_COMMANDS: List[str] = []
|
|
70
|
+
CHOICES_MENUS: List[str] = []
|
|
71
|
+
NEWS_CHOICES: dict = {}
|
|
72
|
+
COMMAND_SEPARATOR = "/"
|
|
73
|
+
KEYS_MENU = "keys" + COMMAND_SEPARATOR
|
|
74
|
+
PATH: str = ""
|
|
75
|
+
FILE_PATH: str = ""
|
|
76
|
+
CHOICES_GENERATION = False
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def choices_default(self):
|
|
80
|
+
"""Return the default choices."""
|
|
81
|
+
choices = (
|
|
82
|
+
build_controller_choice_map(controller=self)
|
|
83
|
+
if self.CHOICES_GENERATION
|
|
84
|
+
else {}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return choices
|
|
88
|
+
|
|
89
|
+
def __init__(self, queue: Optional[List[str]] = None) -> None:
|
|
90
|
+
"""Create the base class for any controller in the codebase.
|
|
91
|
+
|
|
92
|
+
Used to simplify the creation of menus.
|
|
93
|
+
|
|
94
|
+
queue: List[str]
|
|
95
|
+
The current queue of jobs to process separated by "/"
|
|
96
|
+
E.g. /stocks/load gme/dps/sidtc/../exit
|
|
97
|
+
"""
|
|
98
|
+
self.check_path()
|
|
99
|
+
self.path = [x for x in self.PATH.split("/") if x != ""]
|
|
100
|
+
self.queue = (
|
|
101
|
+
self.parse_input(an_input="/".join(queue))
|
|
102
|
+
if (queue and self.PATH != "/")
|
|
103
|
+
else list()
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
controller_choices = self.CHOICES_COMMANDS + self.CHOICES_MENUS
|
|
107
|
+
if controller_choices:
|
|
108
|
+
self.controller_choices = controller_choices + self.CHOICES_COMMON
|
|
109
|
+
else:
|
|
110
|
+
self.controller_choices = self.CHOICES_COMMON
|
|
111
|
+
|
|
112
|
+
self.completer: Union[None, NestedCompleter] = None
|
|
113
|
+
|
|
114
|
+
self.parser = argparse.ArgumentParser(
|
|
115
|
+
add_help=False,
|
|
116
|
+
prog=self.path[-1] if self.PATH != "/" else "cli",
|
|
117
|
+
)
|
|
118
|
+
self.parser.exit_on_error = False # type: ignore
|
|
119
|
+
self.parser.add_argument("cmd", choices=self.controller_choices)
|
|
120
|
+
|
|
121
|
+
def update_completer(self, choices) -> None:
|
|
122
|
+
"""Update the completer with new choices."""
|
|
123
|
+
if session.prompt_session and session.settings.USE_PROMPT_TOOLKIT:
|
|
124
|
+
self.completer = NestedCompleter.from_nested_dict(choices)
|
|
125
|
+
|
|
126
|
+
def check_path(self) -> None:
|
|
127
|
+
"""Check if command path is valid."""
|
|
128
|
+
path = self.PATH
|
|
129
|
+
if path[0] != "/":
|
|
130
|
+
raise ValueError("Path must begin with a '/' character.")
|
|
131
|
+
if path[-1] != "/":
|
|
132
|
+
raise ValueError("Path must end with a '/' character.")
|
|
133
|
+
if not re.match("^[a-z/]*$", path):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"Path must only contain lowercase letters and '/' characters."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def load_class(self, class_ins, *args, **kwargs):
|
|
139
|
+
"""Check for an existing instance of the controller before creating a new one."""
|
|
140
|
+
settings = session.settings
|
|
141
|
+
self.save_class()
|
|
142
|
+
arguments = len(args) + len(kwargs)
|
|
143
|
+
# Due to the 'arguments == 1' condition, we actually NEVER load a class
|
|
144
|
+
# that has arguments (The 1 argument corresponds to self.queue)
|
|
145
|
+
# Advantage: If the user changes something on one controller and then goes to the
|
|
146
|
+
# controller below, it will create such class from scratch bringing all new variables
|
|
147
|
+
# in and considering latest changes.
|
|
148
|
+
# Disadvantage: If the user goes on a controller below and we have been there before
|
|
149
|
+
# it will not load that previous class, but create a new one from scratch.
|
|
150
|
+
# SCENARIO: If the user is in stocks and does load AAPL/ta the TA menu will get AAPL,
|
|
151
|
+
# and if then the user goes back to the stocks menu using .. that menu will have AAPL
|
|
152
|
+
# Now, if "arguments == 1" condition exists, if the user does "load TSLA" and then
|
|
153
|
+
# goes into "TA", the "TSLA" ticker will appear. If that condition doesn't exist
|
|
154
|
+
# the previous class will be loaded and even if the user changes the ticker on
|
|
155
|
+
# the stocks context it will not impact the one of TA menu - unless changes are done.
|
|
156
|
+
if (
|
|
157
|
+
class_ins.PATH in controllers
|
|
158
|
+
and arguments == 1
|
|
159
|
+
and settings.REMEMBER_CONTEXTS
|
|
160
|
+
):
|
|
161
|
+
old_class = controllers[class_ins.PATH]
|
|
162
|
+
old_class.queue = self.queue
|
|
163
|
+
return old_class.menu()
|
|
164
|
+
return class_ins(*args, **kwargs).menu()
|
|
165
|
+
|
|
166
|
+
def save_class(self) -> None:
|
|
167
|
+
"""Save the current instance of the class to be loaded later."""
|
|
168
|
+
if session.settings.REMEMBER_CONTEXTS:
|
|
169
|
+
controllers[self.PATH] = self
|
|
170
|
+
|
|
171
|
+
def custom_reset(self) -> List[str]:
|
|
172
|
+
"""Implement custom reset.
|
|
173
|
+
|
|
174
|
+
This will be replaced by any children with custom_reset functions.
|
|
175
|
+
"""
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def print_help(self) -> None:
|
|
180
|
+
"""Print help placeholder."""
|
|
181
|
+
raise NotImplementedError("Must override print_help.")
|
|
182
|
+
|
|
183
|
+
def parse_input(self, an_input: str) -> list:
|
|
184
|
+
"""Parse controller input.
|
|
185
|
+
|
|
186
|
+
Splits the command chain from user input into a list of individual commands
|
|
187
|
+
while respecting the forward slash in the command arguments.
|
|
188
|
+
|
|
189
|
+
In the default scenario only unix-like paths are handles by the parser.
|
|
190
|
+
Override this function in the controller classes that inherit from this one to
|
|
191
|
+
resolve edge cases specific to command arguments on those controllers.
|
|
192
|
+
|
|
193
|
+
When handling edge cases add additional regular expressions to the list.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
an_input : str
|
|
198
|
+
User input string
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
----------
|
|
202
|
+
list
|
|
203
|
+
Command queue as list
|
|
204
|
+
"""
|
|
205
|
+
custom_filters: list = []
|
|
206
|
+
commands = parse_and_split_input(
|
|
207
|
+
an_input=an_input, custom_filters=custom_filters
|
|
208
|
+
)
|
|
209
|
+
return commands
|
|
210
|
+
|
|
211
|
+
def switch(self, an_input: str) -> List[str]:
|
|
212
|
+
"""Process and dispatch input.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
----------
|
|
216
|
+
List[str]
|
|
217
|
+
list of commands in the queue to execute
|
|
218
|
+
"""
|
|
219
|
+
actions = self.parse_input(an_input)
|
|
220
|
+
|
|
221
|
+
if an_input and an_input != "reset":
|
|
222
|
+
session.console.print()
|
|
223
|
+
|
|
224
|
+
# Empty command
|
|
225
|
+
if len(actions) == 0:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
# Navigation slash is being used first split commands
|
|
229
|
+
elif len(actions) > 1:
|
|
230
|
+
# Absolute path is specified
|
|
231
|
+
if not actions[0]:
|
|
232
|
+
actions[0] = "home"
|
|
233
|
+
|
|
234
|
+
# Add all instructions to the queue
|
|
235
|
+
for cmd in actions[::-1]:
|
|
236
|
+
if cmd:
|
|
237
|
+
self.queue.insert(0, cmd)
|
|
238
|
+
|
|
239
|
+
# Single command fed, process
|
|
240
|
+
else:
|
|
241
|
+
try:
|
|
242
|
+
(known_args, other_args) = self.parser.parse_known_args(
|
|
243
|
+
an_input.split()
|
|
244
|
+
)
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
raise SystemExit from exc
|
|
247
|
+
|
|
248
|
+
if RECORD_SESSION:
|
|
249
|
+
SESSION_RECORDED.append(an_input)
|
|
250
|
+
|
|
251
|
+
# Redirect commands to their correct functions
|
|
252
|
+
if known_args.cmd:
|
|
253
|
+
if known_args.cmd in ("..", "q"):
|
|
254
|
+
known_args.cmd = "quit"
|
|
255
|
+
elif known_args.cmd in ("e"):
|
|
256
|
+
known_args.cmd = "exit"
|
|
257
|
+
elif known_args.cmd in ("?", "h"):
|
|
258
|
+
known_args.cmd = "help"
|
|
259
|
+
elif known_args.cmd == "r":
|
|
260
|
+
known_args.cmd = "reset"
|
|
261
|
+
|
|
262
|
+
getattr(
|
|
263
|
+
self,
|
|
264
|
+
"call_" + known_args.cmd,
|
|
265
|
+
lambda _: "Command not recognized!",
|
|
266
|
+
)(other_args)
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
an_input
|
|
270
|
+
and an_input != "reset"
|
|
271
|
+
and (
|
|
272
|
+
not self.queue or (self.queue and self.queue[0] not in ("quit", "help"))
|
|
273
|
+
)
|
|
274
|
+
):
|
|
275
|
+
session.console.print()
|
|
276
|
+
|
|
277
|
+
return self.queue
|
|
278
|
+
|
|
279
|
+
def call_cls(self, _) -> None:
|
|
280
|
+
"""Process cls command."""
|
|
281
|
+
system_clear()
|
|
282
|
+
|
|
283
|
+
def call_home(self, _) -> None:
|
|
284
|
+
"""Process home command."""
|
|
285
|
+
self.save_class()
|
|
286
|
+
if self.PATH.count("/") == 1 and session.settings.ENABLE_EXIT_AUTO_HELP:
|
|
287
|
+
self.print_help()
|
|
288
|
+
for _ in range(self.PATH.count("/") - 1):
|
|
289
|
+
self.queue.insert(0, "quit")
|
|
290
|
+
|
|
291
|
+
def call_help(self, _) -> None:
|
|
292
|
+
"""Process help command."""
|
|
293
|
+
self.print_help()
|
|
294
|
+
|
|
295
|
+
def call_quit(self, _) -> None:
|
|
296
|
+
"""Process quit menu command."""
|
|
297
|
+
self.save_class()
|
|
298
|
+
self.queue.insert(0, "quit")
|
|
299
|
+
|
|
300
|
+
def call_exit(self, _) -> None:
|
|
301
|
+
# Not sure how to handle controller loading here
|
|
302
|
+
"""Process exit cli command."""
|
|
303
|
+
self.save_class()
|
|
304
|
+
for _ in range(self.PATH.count("/")):
|
|
305
|
+
self.queue.insert(0, "quit")
|
|
306
|
+
|
|
307
|
+
if not session.is_local():
|
|
308
|
+
remove_file(
|
|
309
|
+
Path(session.user.preferences.export_directory, "routines", "hub")
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def call_reset(self, _) -> None:
|
|
313
|
+
"""Process reset command.
|
|
314
|
+
|
|
315
|
+
If you would like to have customization in the reset process define a method
|
|
316
|
+
`custom_reset` in the child class.
|
|
317
|
+
"""
|
|
318
|
+
self.save_class()
|
|
319
|
+
if self.PATH != "/":
|
|
320
|
+
if self.custom_reset():
|
|
321
|
+
self.queue = self.custom_reset() + self.queue
|
|
322
|
+
else:
|
|
323
|
+
for val in self.path[::-1]:
|
|
324
|
+
self.queue.insert(0, val)
|
|
325
|
+
self.queue.insert(0, "reset")
|
|
326
|
+
for _ in range(len(self.path)):
|
|
327
|
+
self.queue.insert(0, "quit")
|
|
328
|
+
|
|
329
|
+
def call_record(self, other_args) -> None:
|
|
330
|
+
"""Process record command."""
|
|
331
|
+
parser = argparse.ArgumentParser(
|
|
332
|
+
add_help=False,
|
|
333
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
334
|
+
prog="record",
|
|
335
|
+
description="Start recording session into .openbb routine file",
|
|
336
|
+
)
|
|
337
|
+
parser.add_argument(
|
|
338
|
+
"-n",
|
|
339
|
+
"--name",
|
|
340
|
+
action="store",
|
|
341
|
+
dest="name",
|
|
342
|
+
type=str,
|
|
343
|
+
default="",
|
|
344
|
+
help="Routine title name to be saved - only use characters, digits and whitespaces.",
|
|
345
|
+
nargs="+",
|
|
346
|
+
)
|
|
347
|
+
parser.add_argument(
|
|
348
|
+
"-d",
|
|
349
|
+
"--description",
|
|
350
|
+
type=str,
|
|
351
|
+
dest="description",
|
|
352
|
+
help="The description of the routine",
|
|
353
|
+
default=f"Routine recorded at {datetime.now().strftime('%H:%M')} from the OpenBB Platform CLI",
|
|
354
|
+
nargs="+",
|
|
355
|
+
)
|
|
356
|
+
parser.add_argument(
|
|
357
|
+
"--tag1",
|
|
358
|
+
type=str,
|
|
359
|
+
dest="tag1",
|
|
360
|
+
help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
|
|
361
|
+
default="",
|
|
362
|
+
nargs="+",
|
|
363
|
+
)
|
|
364
|
+
parser.add_argument(
|
|
365
|
+
"--tag2",
|
|
366
|
+
type=str,
|
|
367
|
+
dest="tag2",
|
|
368
|
+
help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
|
|
369
|
+
default="",
|
|
370
|
+
nargs="+",
|
|
371
|
+
)
|
|
372
|
+
parser.add_argument(
|
|
373
|
+
"--tag3",
|
|
374
|
+
type=str,
|
|
375
|
+
dest="tag3",
|
|
376
|
+
help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
|
|
377
|
+
default="",
|
|
378
|
+
nargs="+",
|
|
379
|
+
)
|
|
380
|
+
parser.add_argument(
|
|
381
|
+
"-p",
|
|
382
|
+
"--public",
|
|
383
|
+
dest="public",
|
|
384
|
+
action="store_true",
|
|
385
|
+
help="Whether the routine should be public or not",
|
|
386
|
+
default=False,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if other_args and "-" not in other_args[0][0]:
|
|
390
|
+
other_args.insert(0, "-n")
|
|
391
|
+
|
|
392
|
+
ns_parser = self.parse_simple_args(parser, other_args)
|
|
393
|
+
|
|
394
|
+
if ns_parser:
|
|
395
|
+
if not ns_parser.name:
|
|
396
|
+
session.console.print(
|
|
397
|
+
"[red]Set a routine title by using the '-n' flag. E.g. 'record -n Morning routine'[/red]"
|
|
398
|
+
)
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
tag1 = (
|
|
402
|
+
" ".join(ns_parser.tag1)
|
|
403
|
+
if isinstance(ns_parser.tag1, list)
|
|
404
|
+
else ns_parser.tag1
|
|
405
|
+
)
|
|
406
|
+
if tag1 and tag1 not in SCRIPT_TAGS:
|
|
407
|
+
session.console.print(
|
|
408
|
+
f"[red]The parameter 'tag1' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
|
|
409
|
+
)
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
tag2 = (
|
|
413
|
+
" ".join(ns_parser.tag2)
|
|
414
|
+
if isinstance(ns_parser.tag2, list)
|
|
415
|
+
else ns_parser.tag2
|
|
416
|
+
)
|
|
417
|
+
if tag2 and tag2 not in SCRIPT_TAGS:
|
|
418
|
+
session.console.print(
|
|
419
|
+
f"[red]The parameter 'tag2' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
|
|
420
|
+
)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
tag3 = (
|
|
424
|
+
" ".join(ns_parser.tag3)
|
|
425
|
+
if isinstance(ns_parser.tag3, list)
|
|
426
|
+
else ns_parser.tag3
|
|
427
|
+
)
|
|
428
|
+
if tag3 and tag3 not in SCRIPT_TAGS:
|
|
429
|
+
session.console.print(
|
|
430
|
+
f"[red]The parameter 'tag3' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
|
|
431
|
+
)
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
if session.is_local():
|
|
435
|
+
session.console.print(
|
|
436
|
+
"[red]Recording session to the OpenBB Hub is not supported in guest mode.[/red]"
|
|
437
|
+
)
|
|
438
|
+
session.console.print(
|
|
439
|
+
"\n[yellow]Visit the OpenBB Hub to register: http://my.openbb.co[/yellow]"
|
|
440
|
+
)
|
|
441
|
+
session.console.print(
|
|
442
|
+
"\n[yellow]Your routine will be saved locally.[/yellow]\n"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Check if title has a valid format
|
|
446
|
+
title = " ".join(ns_parser.name) if ns_parser.name else ""
|
|
447
|
+
pattern = re.compile(r"^[a-zA-Z0-9\s]+$")
|
|
448
|
+
if not pattern.match(title):
|
|
449
|
+
session.console.print(
|
|
450
|
+
f"[red]Title '{title}' has invalid format. Please use only digits, characters and whitespaces.[/]"
|
|
451
|
+
)
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
global RECORD_SESSION # noqa: PLW0603
|
|
455
|
+
global RECORD_SESSION_LOCAL_ONLY # noqa: PLW0603
|
|
456
|
+
global SESSION_RECORDED_NAME # noqa: PLW0603
|
|
457
|
+
global SESSION_RECORDED_DESCRIPTION # noqa: PLW0603
|
|
458
|
+
global SESSION_RECORDED_TAGS # noqa: PLW0603
|
|
459
|
+
global SESSION_RECORDED_PUBLIC # noqa: PLW0603
|
|
460
|
+
|
|
461
|
+
RECORD_SESSION_LOCAL_ONLY = session.is_local()
|
|
462
|
+
RECORD_SESSION = True
|
|
463
|
+
SESSION_RECORDED_NAME = title
|
|
464
|
+
SESSION_RECORDED_DESCRIPTION = (
|
|
465
|
+
" ".join(ns_parser.description)
|
|
466
|
+
if isinstance(ns_parser.description, list)
|
|
467
|
+
else ns_parser.description
|
|
468
|
+
)
|
|
469
|
+
SESSION_RECORDED_TAGS = tag1 if tag1 else ""
|
|
470
|
+
SESSION_RECORDED_TAGS += "," + tag2 if tag2 else ""
|
|
471
|
+
SESSION_RECORDED_TAGS += "," + tag3 if tag3 else ""
|
|
472
|
+
|
|
473
|
+
SESSION_RECORDED_PUBLIC = ns_parser.public
|
|
474
|
+
|
|
475
|
+
session.console.print(
|
|
476
|
+
f"[green]The routine '{title}' is successfully being recorded.[/green]"
|
|
477
|
+
)
|
|
478
|
+
session.console.print(
|
|
479
|
+
"\n[yellow]Remember to run 'stop' command when you are done!\n[/yellow]"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def call_stop(self, other_args) -> None:
|
|
483
|
+
"""Process stop command."""
|
|
484
|
+
parser = argparse.ArgumentParser(
|
|
485
|
+
add_help=False,
|
|
486
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
487
|
+
prog="stop",
|
|
488
|
+
description="Stop recording session into .openbb routine file",
|
|
489
|
+
)
|
|
490
|
+
# This is only for auto-completion purposes
|
|
491
|
+
_ = self.parse_simple_args(parser, other_args)
|
|
492
|
+
|
|
493
|
+
if "-h" not in other_args and "--help" not in other_args:
|
|
494
|
+
global RECORD_SESSION # noqa: PLW0603
|
|
495
|
+
global SESSION_RECORDED # noqa: PLW0603
|
|
496
|
+
|
|
497
|
+
if not RECORD_SESSION:
|
|
498
|
+
session.console.print(
|
|
499
|
+
"[red]There is no session being recorded. Start one using the command 'record'[/red]\n"
|
|
500
|
+
)
|
|
501
|
+
elif len(SESSION_RECORDED) < 5:
|
|
502
|
+
session.console.print(
|
|
503
|
+
"[red]Run at least 4 commands before stopping recording a session.[/red]\n"
|
|
504
|
+
)
|
|
505
|
+
else:
|
|
506
|
+
current_user = session.user
|
|
507
|
+
|
|
508
|
+
# Check if the user just wants to store routine locally
|
|
509
|
+
# This works regardless of whether they are logged in or not
|
|
510
|
+
if RECORD_SESSION_LOCAL_ONLY:
|
|
511
|
+
# Whitespaces are replaced by underscores and an .openbb extension is added
|
|
512
|
+
title_for_local_storage = (
|
|
513
|
+
SESSION_RECORDED_NAME.replace(" ", "_") + ".openbb"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
routine_file = os.path.join(
|
|
517
|
+
f"{current_user.preferences.export_directory}/routines",
|
|
518
|
+
title_for_local_storage,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# If file already exists, add a timestamp to the name
|
|
522
|
+
if os.path.isfile(routine_file):
|
|
523
|
+
i = session.console.input(
|
|
524
|
+
"A local routine with the same name already exists, "
|
|
525
|
+
"do you want to override it? (y/n): "
|
|
526
|
+
)
|
|
527
|
+
session.console.print("")
|
|
528
|
+
while i.lower() not in ["y", "yes", "n", "no"]:
|
|
529
|
+
i = session.console.input("Select 'y' or 'n' to proceed: ")
|
|
530
|
+
session.console.print("")
|
|
531
|
+
|
|
532
|
+
if i.lower() in ["n", "no"]:
|
|
533
|
+
new_name = (
|
|
534
|
+
datetime.now().strftime("%Y%m%d_%H%M%S_")
|
|
535
|
+
+ title_for_local_storage
|
|
536
|
+
)
|
|
537
|
+
routine_file = os.path.join(
|
|
538
|
+
current_user.preferences.export_directory,
|
|
539
|
+
"routines",
|
|
540
|
+
new_name,
|
|
541
|
+
)
|
|
542
|
+
session.console.print(
|
|
543
|
+
f"[yellow]The routine name has been updated to '{new_name}'[/yellow]\n"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Writing to file
|
|
547
|
+
Path(os.path.dirname(routine_file)).mkdir(
|
|
548
|
+
parents=True, exist_ok=True
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
with open(routine_file, "w") as file1:
|
|
552
|
+
lines = ["# OpenBB Platform CLI - Routine", "\n"]
|
|
553
|
+
|
|
554
|
+
username = getattr(
|
|
555
|
+
session.user.profile.hub_session, "username", "local"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
lines += (
|
|
559
|
+
[f"# Author: {username}", "\n\n"] if username else ["\n"]
|
|
560
|
+
)
|
|
561
|
+
lines += [
|
|
562
|
+
f"# Title: {SESSION_RECORDED_NAME}",
|
|
563
|
+
"\n",
|
|
564
|
+
f"# Tags: {SESSION_RECORDED_TAGS}",
|
|
565
|
+
"\n\n",
|
|
566
|
+
f"# Description: {SESSION_RECORDED_DESCRIPTION}",
|
|
567
|
+
"\n\n",
|
|
568
|
+
]
|
|
569
|
+
lines += [c + "\n" for c in SESSION_RECORDED[:-1]]
|
|
570
|
+
# Writing data to a file
|
|
571
|
+
file1.writelines(lines)
|
|
572
|
+
|
|
573
|
+
session.console.print(
|
|
574
|
+
f"[green]Your routine has been recorded and saved here: {routine_file}[/green]\n"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# If user doesn't specify they want to store routine locally
|
|
578
|
+
# Confirm that the user is logged in
|
|
579
|
+
elif not session.is_local():
|
|
580
|
+
routine = "\n".join(SESSION_RECORDED[:-1])
|
|
581
|
+
hub_session = current_user.profile.hub_session
|
|
582
|
+
|
|
583
|
+
if routine is not None:
|
|
584
|
+
auth_header = (
|
|
585
|
+
f"{hub_session.token_type} {hub_session.access_token.get_secret_value()}"
|
|
586
|
+
if hub_session
|
|
587
|
+
else None
|
|
588
|
+
)
|
|
589
|
+
kwargs = {
|
|
590
|
+
"auth_header": auth_header,
|
|
591
|
+
"name": SESSION_RECORDED_NAME,
|
|
592
|
+
"description": SESSION_RECORDED_DESCRIPTION,
|
|
593
|
+
"routine": routine,
|
|
594
|
+
"tags": SESSION_RECORDED_TAGS,
|
|
595
|
+
"public": SESSION_RECORDED_PUBLIC,
|
|
596
|
+
}
|
|
597
|
+
response = upload_routine(**kwargs) # type: ignore
|
|
598
|
+
if response is not None and response.status_code == 409:
|
|
599
|
+
i = session.console.input(
|
|
600
|
+
"A routine with the same name already exists, "
|
|
601
|
+
"do you want to replace it? (y/n): "
|
|
602
|
+
)
|
|
603
|
+
session.console.print("")
|
|
604
|
+
if i.lower() in ["y", "yes"]:
|
|
605
|
+
kwargs["override"] = True # type: ignore
|
|
606
|
+
response = upload_routine(**kwargs) # type: ignore
|
|
607
|
+
else:
|
|
608
|
+
session.console.print("[info]Aborted.[/info]")
|
|
609
|
+
|
|
610
|
+
# Clear session to be recorded again
|
|
611
|
+
RECORD_SESSION = False
|
|
612
|
+
SESSION_RECORDED = list()
|
|
613
|
+
|
|
614
|
+
def call_whoami(self, other_args: List[str]) -> None:
|
|
615
|
+
"""Process whoami command."""
|
|
616
|
+
parser = argparse.ArgumentParser(
|
|
617
|
+
add_help=False,
|
|
618
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
619
|
+
prog="whoami",
|
|
620
|
+
description="Show current user",
|
|
621
|
+
)
|
|
622
|
+
ns_parser = self.parse_simple_args(parser, other_args)
|
|
623
|
+
|
|
624
|
+
if ns_parser:
|
|
625
|
+
current_user = session.user
|
|
626
|
+
local_user = session.is_local()
|
|
627
|
+
if not local_user:
|
|
628
|
+
hub_session = current_user.profile.hub_session
|
|
629
|
+
session.console.print(
|
|
630
|
+
f"[info]email:[/info] {hub_session.email if hub_session else 'N/A'}"
|
|
631
|
+
)
|
|
632
|
+
session.console.print(
|
|
633
|
+
f"[info]uuid:[/info] {hub_session.user_uuid if hub_session else 'N/A'}"
|
|
634
|
+
)
|
|
635
|
+
else:
|
|
636
|
+
print_guest_block_msg()
|
|
637
|
+
|
|
638
|
+
def call_results(self, other_args: List[str]):
|
|
639
|
+
"""Process results command."""
|
|
640
|
+
parser = argparse.ArgumentParser(
|
|
641
|
+
add_help=False,
|
|
642
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
643
|
+
prog="results",
|
|
644
|
+
description="Process results command. This command displays a registry of "
|
|
645
|
+
"'OBBjects' where all execution results are stored. "
|
|
646
|
+
"It is organized as a stack, with the most recent result at index 0.",
|
|
647
|
+
)
|
|
648
|
+
ns_parser = self.parse_simple_args(parser, other_args)
|
|
649
|
+
if ns_parser:
|
|
650
|
+
results = session.obbject_registry.all
|
|
651
|
+
if results:
|
|
652
|
+
df = pd.DataFrame.from_dict(results, orient="index")
|
|
653
|
+
print_rich_table(
|
|
654
|
+
df,
|
|
655
|
+
show_index=True,
|
|
656
|
+
index_name="stack index",
|
|
657
|
+
title="OBBject Results",
|
|
658
|
+
)
|
|
659
|
+
else:
|
|
660
|
+
session.console.print("[info]No results found.[/info]")
|
|
661
|
+
|
|
662
|
+
@staticmethod
|
|
663
|
+
def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]):
|
|
664
|
+
"""Parse list of arguments into the supplied parser.
|
|
665
|
+
|
|
666
|
+
Parameters
|
|
667
|
+
----------
|
|
668
|
+
parser: argparse.ArgumentParser
|
|
669
|
+
Parser with predefined arguments
|
|
670
|
+
other_args: List[str]
|
|
671
|
+
List of arguments to parse
|
|
672
|
+
|
|
673
|
+
Returns
|
|
674
|
+
-------
|
|
675
|
+
ns_parser:
|
|
676
|
+
Namespace with parsed arguments
|
|
677
|
+
"""
|
|
678
|
+
parser.add_argument(
|
|
679
|
+
"-h", "--help", action="store_true", help="show this help message"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
if session.settings.USE_CLEAR_AFTER_CMD:
|
|
683
|
+
system_clear()
|
|
684
|
+
|
|
685
|
+
try:
|
|
686
|
+
(ns_parser, l_unknown_args) = parser.parse_known_args(other_args)
|
|
687
|
+
except SystemExit:
|
|
688
|
+
# In case the command has required argument that isn't specified
|
|
689
|
+
session.console.print("\n")
|
|
690
|
+
return None
|
|
691
|
+
|
|
692
|
+
if ns_parser.help:
|
|
693
|
+
txt_help = parser.format_help()
|
|
694
|
+
session.console.print(f"[help]{txt_help}[/help]")
|
|
695
|
+
return None
|
|
696
|
+
|
|
697
|
+
if l_unknown_args:
|
|
698
|
+
session.console.print(
|
|
699
|
+
f"The following args couldn't be interpreted: {l_unknown_args}\n"
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
return ns_parser
|
|
703
|
+
|
|
704
|
+
@classmethod
|
|
705
|
+
def parse_known_args_and_warn(
|
|
706
|
+
cls,
|
|
707
|
+
parser: argparse.ArgumentParser,
|
|
708
|
+
other_args: List[str],
|
|
709
|
+
export_allowed: Literal[
|
|
710
|
+
"no_export", "raw_data_only", "figures_only", "raw_data_and_figures"
|
|
711
|
+
] = "no_export",
|
|
712
|
+
raw: bool = False,
|
|
713
|
+
limit: int = 0,
|
|
714
|
+
):
|
|
715
|
+
"""Parse list of arguments into the supplied parser.
|
|
716
|
+
|
|
717
|
+
Parameters
|
|
718
|
+
----------
|
|
719
|
+
parser: argparse.ArgumentParser
|
|
720
|
+
Parser with predefined arguments
|
|
721
|
+
other_args: List[str]
|
|
722
|
+
list of arguments to parse
|
|
723
|
+
export_allowed: Literal["no_export", "raw_data_only", "figures_only", "raw_data_and_figures"]
|
|
724
|
+
Export options
|
|
725
|
+
raw: bool
|
|
726
|
+
Add the --raw flag
|
|
727
|
+
limit: int
|
|
728
|
+
Add a --limit flag with this number default
|
|
729
|
+
|
|
730
|
+
Returns
|
|
731
|
+
----------
|
|
732
|
+
ns_parser:
|
|
733
|
+
Namespace with parsed arguments
|
|
734
|
+
"""
|
|
735
|
+
parser.add_argument(
|
|
736
|
+
"-h", "--help", action="store_true", help="show this help message"
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
if export_allowed != "no_export":
|
|
740
|
+
choices_export = []
|
|
741
|
+
help_export = "Does not export!"
|
|
742
|
+
|
|
743
|
+
if export_allowed == "raw_data_only":
|
|
744
|
+
choices_export = ["csv", "json", "xlsx"]
|
|
745
|
+
help_export = "Export raw data into csv, json, xlsx"
|
|
746
|
+
elif export_allowed == "figures_only":
|
|
747
|
+
choices_export = ["png", "jpg", "pdf", "svg"]
|
|
748
|
+
help_export = "Export figure into png, jpg, pdf, svg "
|
|
749
|
+
else:
|
|
750
|
+
choices_export = ["csv", "json", "xlsx", "png", "jpg", "pdf", "svg"]
|
|
751
|
+
help_export = "Export raw data into csv, json, xlsx and figure into png, jpg, pdf, svg "
|
|
752
|
+
|
|
753
|
+
parser.add_argument(
|
|
754
|
+
"--export",
|
|
755
|
+
default="",
|
|
756
|
+
type=check_file_type_saved(choices_export),
|
|
757
|
+
dest="export",
|
|
758
|
+
help=help_export,
|
|
759
|
+
nargs="+",
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
# If excel is an option, add the sheet name
|
|
763
|
+
if export_allowed in [
|
|
764
|
+
"raw_data_only",
|
|
765
|
+
"raw_data_and_figures",
|
|
766
|
+
]:
|
|
767
|
+
parser.add_argument(
|
|
768
|
+
"--sheet-name",
|
|
769
|
+
dest="sheet_name",
|
|
770
|
+
default=None,
|
|
771
|
+
nargs="+",
|
|
772
|
+
help="Name of excel sheet to save data to. Only valid for .xlsx files.",
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
if raw:
|
|
776
|
+
parser.add_argument(
|
|
777
|
+
"--raw",
|
|
778
|
+
dest="raw",
|
|
779
|
+
action="store_true",
|
|
780
|
+
default=False,
|
|
781
|
+
help="Flag to display raw data",
|
|
782
|
+
)
|
|
783
|
+
if limit > 0:
|
|
784
|
+
parser.add_argument(
|
|
785
|
+
"-l",
|
|
786
|
+
"--limit",
|
|
787
|
+
dest="limit",
|
|
788
|
+
default=limit,
|
|
789
|
+
help="Number of entries to show in data.",
|
|
790
|
+
type=check_positive,
|
|
791
|
+
)
|
|
792
|
+
if session.settings.USE_CLEAR_AFTER_CMD:
|
|
793
|
+
system_clear()
|
|
794
|
+
|
|
795
|
+
if "--help" in other_args or "-h" in other_args:
|
|
796
|
+
txt_help = parser.format_help() + "\n"
|
|
797
|
+
session.console.print(f"[help]{txt_help}[/help]")
|
|
798
|
+
return None
|
|
799
|
+
|
|
800
|
+
try:
|
|
801
|
+
# If the user uses a comma separated list of arguments, split them
|
|
802
|
+
for index, arg in enumerate(other_args):
|
|
803
|
+
if "," in arg:
|
|
804
|
+
parts = arg.split(",")
|
|
805
|
+
other_args[index : index + 1] = parts
|
|
806
|
+
|
|
807
|
+
(ns_parser, l_unknown_args) = parser.parse_known_args(other_args)
|
|
808
|
+
|
|
809
|
+
if export_allowed in [
|
|
810
|
+
"raw_data_only",
|
|
811
|
+
"raw_data_and_figures",
|
|
812
|
+
]:
|
|
813
|
+
ns_parser.is_image = any(
|
|
814
|
+
ext in ns_parser.export for ext in ["png", "svg", "jpg", "pdf"]
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
except SystemExit:
|
|
818
|
+
# In case the command has required argument that isn't specified
|
|
819
|
+
|
|
820
|
+
return None
|
|
821
|
+
|
|
822
|
+
if l_unknown_args:
|
|
823
|
+
session.console.print(
|
|
824
|
+
f"The following args couldn't be interpreted: {l_unknown_args}"
|
|
825
|
+
)
|
|
826
|
+
return ns_parser
|
|
827
|
+
|
|
828
|
+
def menu(self, custom_path_menu_above: str = ""):
|
|
829
|
+
"""Enter controller menu."""
|
|
830
|
+
settings = session.settings
|
|
831
|
+
an_input = "HELP_ME"
|
|
832
|
+
|
|
833
|
+
while True:
|
|
834
|
+
# There is a command in the queue
|
|
835
|
+
if self.queue and len(self.queue) > 0:
|
|
836
|
+
if self.queue[0] in ("q", "..", "quit"):
|
|
837
|
+
self.save_class()
|
|
838
|
+
# Go back to the root in order to go to the right directory because
|
|
839
|
+
# there was a jump between indirect menus
|
|
840
|
+
if custom_path_menu_above:
|
|
841
|
+
self.queue.insert(1, custom_path_menu_above)
|
|
842
|
+
|
|
843
|
+
if len(self.queue) > 1:
|
|
844
|
+
return self.queue[1:]
|
|
845
|
+
|
|
846
|
+
if settings.ENABLE_EXIT_AUTO_HELP:
|
|
847
|
+
return ["help"]
|
|
848
|
+
return []
|
|
849
|
+
|
|
850
|
+
# Consume 1 element from the queue
|
|
851
|
+
an_input = self.queue[0]
|
|
852
|
+
self.queue = self.queue[1:]
|
|
853
|
+
|
|
854
|
+
# Print location because this was an instruction and we want user to know the action
|
|
855
|
+
if (
|
|
856
|
+
an_input
|
|
857
|
+
and an_input != "home"
|
|
858
|
+
and an_input != "help"
|
|
859
|
+
and an_input.split(" ")[0] in self.controller_choices
|
|
860
|
+
):
|
|
861
|
+
session.console.print(
|
|
862
|
+
f"{get_flair_and_username()} {self.PATH} $ {an_input}"
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# Get input command from user
|
|
866
|
+
else:
|
|
867
|
+
# Display help menu when entering on this menu from a level above
|
|
868
|
+
if an_input == "HELP_ME":
|
|
869
|
+
self.print_help()
|
|
870
|
+
|
|
871
|
+
try:
|
|
872
|
+
prompt_session = session.prompt_session
|
|
873
|
+
if prompt_session and settings.USE_PROMPT_TOOLKIT:
|
|
874
|
+
# Check if toolbar hint was enabled
|
|
875
|
+
if settings.TOOLBAR_HINT:
|
|
876
|
+
an_input = prompt_session.prompt(
|
|
877
|
+
f"{get_flair_and_username()} {self.PATH} $ ",
|
|
878
|
+
completer=self.completer,
|
|
879
|
+
search_ignore_case=True,
|
|
880
|
+
bottom_toolbar=HTML(
|
|
881
|
+
'<style bg="ansiblack" fg="ansiwhite">[h]</style> help menu '
|
|
882
|
+
'<style bg="ansiblack" fg="ansiwhite">[q]</style> return to previous menu '
|
|
883
|
+
'<style bg="ansiblack" fg="ansiwhite">[e]</style> exit the program '
|
|
884
|
+
'<style bg="ansiblack" fg="ansiwhite">[cmd -h]</style> '
|
|
885
|
+
"see usage and available options "
|
|
886
|
+
f"{self.path[-1].capitalize()} (cmd/menu) Documentation"
|
|
887
|
+
),
|
|
888
|
+
style=Style.from_dict(
|
|
889
|
+
{"bottom-toolbar": "#ffffff bg:#333333"}
|
|
890
|
+
),
|
|
891
|
+
)
|
|
892
|
+
else:
|
|
893
|
+
an_input = prompt_session.prompt(
|
|
894
|
+
f"{get_flair_and_username()} {self.PATH} $ ",
|
|
895
|
+
completer=self.completer,
|
|
896
|
+
search_ignore_case=True,
|
|
897
|
+
)
|
|
898
|
+
# Get input from user without auto-completion
|
|
899
|
+
else:
|
|
900
|
+
an_input = input(f"{get_flair_and_username()} {self.PATH} $ ")
|
|
901
|
+
|
|
902
|
+
except (KeyboardInterrupt, EOFError):
|
|
903
|
+
# Exit in case of keyboard interrupt
|
|
904
|
+
an_input = "exit"
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
# Allow user to go back to root
|
|
908
|
+
an_input = "home" if an_input == "/" else an_input
|
|
909
|
+
|
|
910
|
+
# Process the input command
|
|
911
|
+
self.queue = self.switch(an_input)
|
|
912
|
+
|
|
913
|
+
except SystemExit:
|
|
914
|
+
session.console.print(
|
|
915
|
+
f"[red]The command '{an_input}' doesn't exist on the {self.PATH} menu.[/red]\n",
|
|
916
|
+
)
|
|
917
|
+
similar_cmd = difflib.get_close_matches(
|
|
918
|
+
an_input.split(" ")[0] if " " in an_input else an_input,
|
|
919
|
+
self.controller_choices,
|
|
920
|
+
n=1,
|
|
921
|
+
cutoff=0.7,
|
|
922
|
+
)
|
|
923
|
+
if similar_cmd:
|
|
924
|
+
if " " in an_input:
|
|
925
|
+
candidate_input = (
|
|
926
|
+
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
|
|
927
|
+
)
|
|
928
|
+
if candidate_input == an_input:
|
|
929
|
+
an_input = ""
|
|
930
|
+
self.queue = []
|
|
931
|
+
session.console.print("\n")
|
|
932
|
+
continue
|
|
933
|
+
|
|
934
|
+
an_input = candidate_input
|
|
935
|
+
else:
|
|
936
|
+
an_input = similar_cmd[0]
|
|
937
|
+
|
|
938
|
+
session.console.print(
|
|
939
|
+
f"[green]Replacing by '{an_input}'.[/green]\n"
|
|
940
|
+
)
|
|
941
|
+
self.queue.insert(0, an_input)
|