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,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": {
|
|
3
|
+
"candlestick": [
|
|
4
|
+
{
|
|
5
|
+
"decreasing": {
|
|
6
|
+
"fillcolor": "#e4003a",
|
|
7
|
+
"line": {
|
|
8
|
+
"color": "#e4003a"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"increasing": {
|
|
12
|
+
"fillcolor": "#00ACFF",
|
|
13
|
+
"line": {
|
|
14
|
+
"color": "#00ACFF"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"type": "candlestick"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"layout": {
|
|
22
|
+
"annotationdefaults": {
|
|
23
|
+
"showarrow": false
|
|
24
|
+
},
|
|
25
|
+
"autotypenumbers": "strict",
|
|
26
|
+
"colorway": [
|
|
27
|
+
"#ffed00",
|
|
28
|
+
"#ef7d00",
|
|
29
|
+
"#e4003a",
|
|
30
|
+
"#c13246",
|
|
31
|
+
"#822661",
|
|
32
|
+
"#48277c",
|
|
33
|
+
"#005ca9",
|
|
34
|
+
"#00aaff",
|
|
35
|
+
"#9b30d9",
|
|
36
|
+
"#af005f",
|
|
37
|
+
"#5f00af",
|
|
38
|
+
"#af87ff"
|
|
39
|
+
],
|
|
40
|
+
"dragmode": "pan",
|
|
41
|
+
"font": {
|
|
42
|
+
"family": "Fira Code",
|
|
43
|
+
"size": 18
|
|
44
|
+
},
|
|
45
|
+
"hoverlabel": {
|
|
46
|
+
"align": "left"
|
|
47
|
+
},
|
|
48
|
+
"mapbox": {
|
|
49
|
+
"style": "light"
|
|
50
|
+
},
|
|
51
|
+
"hovermode": "x",
|
|
52
|
+
"legend": {
|
|
53
|
+
"bgcolor": "rgba(0, 0, 0, 0)",
|
|
54
|
+
"x": 0.01,
|
|
55
|
+
"xanchor": "left",
|
|
56
|
+
"y": 0.99,
|
|
57
|
+
"yanchor": "top",
|
|
58
|
+
"font": {
|
|
59
|
+
"size": 15
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"paper_bgcolor": "white",
|
|
63
|
+
"plot_bgcolor": "white",
|
|
64
|
+
"xaxis": {
|
|
65
|
+
"automargin": true,
|
|
66
|
+
"autorange": true,
|
|
67
|
+
"rangeslider": {
|
|
68
|
+
"visible": false
|
|
69
|
+
},
|
|
70
|
+
"showgrid": true,
|
|
71
|
+
"showline": true,
|
|
72
|
+
"tickfont": {
|
|
73
|
+
"size": 14
|
|
74
|
+
},
|
|
75
|
+
"zeroline": false,
|
|
76
|
+
"tick0": 1,
|
|
77
|
+
"title": {
|
|
78
|
+
"standoff": 20
|
|
79
|
+
},
|
|
80
|
+
"linecolor": "#F5EFF3",
|
|
81
|
+
"mirror": true,
|
|
82
|
+
"ticks": "outside"
|
|
83
|
+
},
|
|
84
|
+
"yaxis": {
|
|
85
|
+
"anchor": "x",
|
|
86
|
+
"automargin": true,
|
|
87
|
+
"fixedrange": false,
|
|
88
|
+
"zeroline": false,
|
|
89
|
+
"showgrid": true,
|
|
90
|
+
"showline": true,
|
|
91
|
+
"side": "right",
|
|
92
|
+
"tick0": 0.5,
|
|
93
|
+
"title": {
|
|
94
|
+
"standoff": 20
|
|
95
|
+
},
|
|
96
|
+
"gridcolor": "#283442",
|
|
97
|
+
"linecolor": "#F5EFF3",
|
|
98
|
+
"mirror": true,
|
|
99
|
+
"ticks": "outside"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
openbb_cli/cli.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""OpenBB Platform CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from openbb_cli.config.setup import bootstrap
|
|
6
|
+
from openbb_cli.controllers.cli_controller import launch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
"""Use the main entry point for the OpenBB CLI."""
|
|
11
|
+
bootstrap()
|
|
12
|
+
|
|
13
|
+
dev = "--dev" in sys.argv[1:]
|
|
14
|
+
debug = "--debug" in sys.argv[1:]
|
|
15
|
+
|
|
16
|
+
launch(dev, debug)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core config init."""
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Nested completer for completion of OpenBB hierarchical data structures."""
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
Dict,
|
|
7
|
+
Iterable,
|
|
8
|
+
List,
|
|
9
|
+
Mapping,
|
|
10
|
+
Optional,
|
|
11
|
+
Pattern,
|
|
12
|
+
Set,
|
|
13
|
+
Union,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
17
|
+
from prompt_toolkit.document import Document
|
|
18
|
+
from prompt_toolkit.formatted_text import AnyFormattedText
|
|
19
|
+
from prompt_toolkit.history import FileHistory
|
|
20
|
+
|
|
21
|
+
NestedDict = Mapping[str, Union[Any, Set[str], None, Completer]]
|
|
22
|
+
|
|
23
|
+
# pylint: disable=too-many-arguments,global-statement,too-many-branches,global-variable-not-assigned
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WordCompleter(Completer):
|
|
27
|
+
"""Simple autocompletion on a list of words.
|
|
28
|
+
|
|
29
|
+
:param words: List of words or callable that returns a list of words.
|
|
30
|
+
:param ignore_case: If True, case-insensitive completion.
|
|
31
|
+
:param meta_dict: Optional dict mapping words to their meta-text. (This
|
|
32
|
+
should map strings to strings or formatted text.)
|
|
33
|
+
:param WORD: When True, use WORD characters.
|
|
34
|
+
:param sentence: When True, don't complete by comparing the word before the
|
|
35
|
+
cursor, but by comparing all the text before the cursor. In this case,
|
|
36
|
+
the list of words is just a list of strings, where each string can
|
|
37
|
+
contain spaces. (Can not be used together with the WORD option.)
|
|
38
|
+
:param match_middle: When True, match not only the start, but also in the
|
|
39
|
+
middle of the word.
|
|
40
|
+
:param pattern: Optional compiled regex for finding the word before
|
|
41
|
+
the cursor to complete. When given, use this regex pattern instead of
|
|
42
|
+
default one (see document._FIND_WORD_RE)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
words: Union[List[str], Callable[[], List[str]]],
|
|
48
|
+
ignore_case: bool = False,
|
|
49
|
+
display_dict: Optional[Mapping[str, AnyFormattedText]] = None,
|
|
50
|
+
meta_dict: Optional[Mapping[str, AnyFormattedText]] = None,
|
|
51
|
+
WORD: bool = True,
|
|
52
|
+
sentence: bool = False,
|
|
53
|
+
match_middle: bool = False,
|
|
54
|
+
pattern: Optional[Pattern[str]] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Initialize the WordCompleter."""
|
|
57
|
+
assert not (WORD and sentence) # noqa: S101
|
|
58
|
+
|
|
59
|
+
self.words = words
|
|
60
|
+
self.ignore_case = ignore_case
|
|
61
|
+
self.display_dict = display_dict or {}
|
|
62
|
+
self.meta_dict = meta_dict or {}
|
|
63
|
+
self.WORD = WORD
|
|
64
|
+
self.sentence = sentence
|
|
65
|
+
self.match_middle = match_middle
|
|
66
|
+
self.pattern = pattern
|
|
67
|
+
|
|
68
|
+
def get_completions(
|
|
69
|
+
self,
|
|
70
|
+
document: Document,
|
|
71
|
+
_complete_event: CompleteEvent,
|
|
72
|
+
) -> Iterable[Completion]:
|
|
73
|
+
"""Get completions."""
|
|
74
|
+
# Get list of words.
|
|
75
|
+
words = self.words
|
|
76
|
+
if callable(words):
|
|
77
|
+
words = words()
|
|
78
|
+
|
|
79
|
+
# Get word/text before cursor.
|
|
80
|
+
if self.sentence:
|
|
81
|
+
word_before_cursor = document.text_before_cursor
|
|
82
|
+
else:
|
|
83
|
+
word_before_cursor = document.get_word_before_cursor(
|
|
84
|
+
WORD=self.WORD, pattern=self.pattern
|
|
85
|
+
)
|
|
86
|
+
if (
|
|
87
|
+
"--" in document.text_before_cursor
|
|
88
|
+
and document.text_before_cursor.rfind(" --")
|
|
89
|
+
>= document.text_before_cursor.rfind(" -")
|
|
90
|
+
):
|
|
91
|
+
word_before_cursor = f'--{document.text_before_cursor.split("--")[-1]}'
|
|
92
|
+
elif f"--{word_before_cursor}" == document.text_before_cursor:
|
|
93
|
+
word_before_cursor = document.text_before_cursor
|
|
94
|
+
|
|
95
|
+
if self.ignore_case:
|
|
96
|
+
word_before_cursor = word_before_cursor.lower()
|
|
97
|
+
|
|
98
|
+
def word_matches(word: str) -> bool:
|
|
99
|
+
"""Set True when the word before the cursor matches."""
|
|
100
|
+
if self.ignore_case:
|
|
101
|
+
word = word.lower()
|
|
102
|
+
|
|
103
|
+
if self.match_middle:
|
|
104
|
+
return word_before_cursor in word
|
|
105
|
+
return word.startswith(word_before_cursor)
|
|
106
|
+
|
|
107
|
+
for a in words:
|
|
108
|
+
if word_matches(a):
|
|
109
|
+
display = self.display_dict.get(a, a)
|
|
110
|
+
display_meta = self.meta_dict.get(a, "")
|
|
111
|
+
yield Completion(
|
|
112
|
+
text=a,
|
|
113
|
+
start_position=-len(word_before_cursor),
|
|
114
|
+
display=display,
|
|
115
|
+
display_meta=display_meta,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class NestedCompleter(Completer):
|
|
120
|
+
"""Completer which wraps around several other completers, and calls any the
|
|
121
|
+
one that corresponds with the first word of the input.
|
|
122
|
+
|
|
123
|
+
By combining multiple `NestedCompleter` instances, we can achieve multiple
|
|
124
|
+
hierarchical levels of autocompletion. This is useful when `WordCompleter`
|
|
125
|
+
is not sufficient.
|
|
126
|
+
|
|
127
|
+
If you need multiple levels, check out the `from_nested_dict` classmethod.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
complementary: List = list()
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self, options: Dict[str, Optional[Completer]], ignore_case: bool = True
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Initialize the NestedCompleter."""
|
|
136
|
+
self.flags_processed: List = list()
|
|
137
|
+
self.original_options = options
|
|
138
|
+
self.options = options
|
|
139
|
+
self.ignore_case = ignore_case
|
|
140
|
+
self.complementary = list()
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str:
|
|
143
|
+
"""Return string representation of NestedCompleter."""
|
|
144
|
+
return f"NestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_nested_dict(cls, data: dict) -> "NestedCompleter":
|
|
148
|
+
"""Create a `NestedCompleter`.
|
|
149
|
+
|
|
150
|
+
It starts from a nested dictionary data structure, like this:
|
|
151
|
+
|
|
152
|
+
.. code::
|
|
153
|
+
|
|
154
|
+
data = {
|
|
155
|
+
'show': {
|
|
156
|
+
'version': None,
|
|
157
|
+
'interfaces': None,
|
|
158
|
+
'clock': None,
|
|
159
|
+
'ip': {'interface': {'brief'}}
|
|
160
|
+
},
|
|
161
|
+
'exit': None
|
|
162
|
+
'enable': None
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
The value should be `None` if there is no further completion at some
|
|
166
|
+
point. If all values in the dictionary are None, it is also possible to
|
|
167
|
+
use a set instead.
|
|
168
|
+
|
|
169
|
+
Values in this data structure can be a completers as well.
|
|
170
|
+
"""
|
|
171
|
+
options: Dict[str, Any] = {}
|
|
172
|
+
for key, value in data.items():
|
|
173
|
+
if isinstance(value, Completer):
|
|
174
|
+
options[key] = value
|
|
175
|
+
elif isinstance(value, dict):
|
|
176
|
+
options[key] = cls.from_nested_dict(value)
|
|
177
|
+
elif isinstance(value, set):
|
|
178
|
+
options[key] = cls.from_nested_dict({item: None for item in value})
|
|
179
|
+
elif isinstance(key, str) and isinstance(value, str):
|
|
180
|
+
options[key] = options[value]
|
|
181
|
+
else:
|
|
182
|
+
assert value is None # noqa: S101
|
|
183
|
+
options[key] = None
|
|
184
|
+
|
|
185
|
+
for items in cls.complementary:
|
|
186
|
+
if items[0] in options:
|
|
187
|
+
options[items[1]] = options[items[0]]
|
|
188
|
+
elif items[1] in options:
|
|
189
|
+
options[items[0]] = options[items[1]]
|
|
190
|
+
|
|
191
|
+
return cls(options)
|
|
192
|
+
|
|
193
|
+
def get_completions( # noqa: PLR0912
|
|
194
|
+
self, document: Document, complete_event: CompleteEvent
|
|
195
|
+
) -> Iterable[Completion]:
|
|
196
|
+
"""Get completions."""
|
|
197
|
+
# Split document.
|
|
198
|
+
cmd = ""
|
|
199
|
+
text = document.text_before_cursor.lstrip()
|
|
200
|
+
if " " in text:
|
|
201
|
+
cmd = text.split(" ")[0]
|
|
202
|
+
if "-" in text:
|
|
203
|
+
if text.rfind("--") == -1 or text.rfind("-") - 1 > text.rfind("--"):
|
|
204
|
+
unprocessed_text = "-" + text.split("-")[-1]
|
|
205
|
+
else:
|
|
206
|
+
unprocessed_text = "--" + text.split("--")[-1]
|
|
207
|
+
else:
|
|
208
|
+
unprocessed_text = text
|
|
209
|
+
stripped_len = len(document.text_before_cursor) - len(text)
|
|
210
|
+
|
|
211
|
+
# Check if there are multiple flags for the same command
|
|
212
|
+
if self.complementary:
|
|
213
|
+
for same_flags in self.complementary:
|
|
214
|
+
if (
|
|
215
|
+
same_flags[0] in self.flags_processed
|
|
216
|
+
and same_flags[1] not in self.flags_processed
|
|
217
|
+
) or (
|
|
218
|
+
same_flags[1] in self.flags_processed
|
|
219
|
+
and same_flags[0] not in self.flags_processed
|
|
220
|
+
):
|
|
221
|
+
if same_flags[0] in self.flags_processed:
|
|
222
|
+
self.flags_processed.append(same_flags[1])
|
|
223
|
+
elif same_flags[1] in self.flags_processed:
|
|
224
|
+
self.flags_processed.append(same_flags[0])
|
|
225
|
+
|
|
226
|
+
if cmd:
|
|
227
|
+
self.options = {
|
|
228
|
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
|
229
|
+
for k in self.original_options.get(cmd).options # type: ignore
|
|
230
|
+
if k not in self.flags_processed
|
|
231
|
+
}
|
|
232
|
+
else:
|
|
233
|
+
self.options = {
|
|
234
|
+
k: self.original_options[k]
|
|
235
|
+
for k in self.original_options
|
|
236
|
+
if k not in self.flags_processed
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# If there is a space, check for the first term, and use a subcompleter.
|
|
240
|
+
if " " in unprocessed_text:
|
|
241
|
+
first_term = unprocessed_text.split()[0]
|
|
242
|
+
|
|
243
|
+
# user is updating one of the values
|
|
244
|
+
if unprocessed_text[-1] != " ":
|
|
245
|
+
self.flags_processed = [
|
|
246
|
+
flag for flag in self.flags_processed if flag != first_term
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
if self.complementary:
|
|
250
|
+
for same_flags in self.complementary:
|
|
251
|
+
if (
|
|
252
|
+
same_flags[0] in self.flags_processed
|
|
253
|
+
and same_flags[1] not in self.flags_processed
|
|
254
|
+
) or (
|
|
255
|
+
same_flags[1] in self.flags_processed
|
|
256
|
+
and same_flags[0] not in self.flags_processed
|
|
257
|
+
):
|
|
258
|
+
if same_flags[0] in self.flags_processed:
|
|
259
|
+
self.flags_processed.remove(same_flags[0])
|
|
260
|
+
elif same_flags[1] in self.flags_processed:
|
|
261
|
+
self.flags_processed.remove(same_flags[1])
|
|
262
|
+
|
|
263
|
+
if cmd and self.original_options.get(cmd):
|
|
264
|
+
self.options = self.original_options
|
|
265
|
+
else:
|
|
266
|
+
self.options = {
|
|
267
|
+
k: self.original_options[k]
|
|
268
|
+
for k in self.original_options
|
|
269
|
+
if k not in self.flags_processed
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if "-" not in text:
|
|
273
|
+
completer = self.options.get(first_term)
|
|
274
|
+
elif cmd in self.options and self.options.get(cmd):
|
|
275
|
+
completer = self.options.get(cmd).options.get(first_term) # type: ignore
|
|
276
|
+
else:
|
|
277
|
+
completer = self.options.get(first_term)
|
|
278
|
+
|
|
279
|
+
# If we have a sub completer, use this for the completions.
|
|
280
|
+
if completer is not None:
|
|
281
|
+
remaining_text = unprocessed_text[len(first_term) :].lstrip()
|
|
282
|
+
move_cursor = len(text) - len(remaining_text) + stripped_len
|
|
283
|
+
|
|
284
|
+
new_document = Document(
|
|
285
|
+
remaining_text,
|
|
286
|
+
cursor_position=document.cursor_position - move_cursor,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Provides auto-completion but if user doesn't take it still keep going
|
|
290
|
+
if " " in new_document.text:
|
|
291
|
+
if (
|
|
292
|
+
new_document.text in [f"{opt} " for opt in self.options]
|
|
293
|
+
or unprocessed_text[-1] == " "
|
|
294
|
+
):
|
|
295
|
+
self.flags_processed.append(first_term)
|
|
296
|
+
if cmd:
|
|
297
|
+
self.options = {
|
|
298
|
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
|
299
|
+
for k in self.original_options.get(cmd).options # type: ignore
|
|
300
|
+
if k not in self.flags_processed
|
|
301
|
+
}
|
|
302
|
+
else:
|
|
303
|
+
self.options = {
|
|
304
|
+
k: self.original_options[k]
|
|
305
|
+
for k in self.original_options
|
|
306
|
+
if k not in self.flags_processed
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# In case the users inputs a single boolean flag
|
|
310
|
+
elif not completer.options: # type: ignore
|
|
311
|
+
self.flags_processed.append(first_term)
|
|
312
|
+
|
|
313
|
+
if self.complementary:
|
|
314
|
+
for same_flags in self.complementary:
|
|
315
|
+
if (
|
|
316
|
+
same_flags[0] in self.flags_processed
|
|
317
|
+
and same_flags[1] not in self.flags_processed
|
|
318
|
+
) or (
|
|
319
|
+
same_flags[1] in self.flags_processed
|
|
320
|
+
and same_flags[0] not in self.flags_processed
|
|
321
|
+
):
|
|
322
|
+
if same_flags[0] in self.flags_processed:
|
|
323
|
+
self.flags_processed.append(same_flags[1])
|
|
324
|
+
elif same_flags[1] in self.flags_processed:
|
|
325
|
+
self.flags_processed.append(same_flags[0])
|
|
326
|
+
|
|
327
|
+
if cmd:
|
|
328
|
+
self.options = {
|
|
329
|
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
|
330
|
+
for k in self.original_options.get(cmd).options # type: ignore
|
|
331
|
+
if k not in self.flags_processed
|
|
332
|
+
}
|
|
333
|
+
else:
|
|
334
|
+
self.options = {
|
|
335
|
+
k: self.original_options[k]
|
|
336
|
+
for k in self.original_options
|
|
337
|
+
if k not in self.flags_processed
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
else:
|
|
341
|
+
# This is a NestedCompleter
|
|
342
|
+
yield from completer.get_completions(new_document, complete_event)
|
|
343
|
+
|
|
344
|
+
# No space in the input: behave exactly like `WordCompleter`.
|
|
345
|
+
else:
|
|
346
|
+
# check if the prompt has been updated in the meantime
|
|
347
|
+
if " " in text or "-" in text:
|
|
348
|
+
actual_flags_processed = [
|
|
349
|
+
flag for flag in self.flags_processed if flag in text
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
if self.complementary:
|
|
353
|
+
for same_flags in self.complementary:
|
|
354
|
+
if (
|
|
355
|
+
same_flags[0] in actual_flags_processed
|
|
356
|
+
and same_flags[1] not in actual_flags_processed
|
|
357
|
+
) or (
|
|
358
|
+
same_flags[1] in actual_flags_processed
|
|
359
|
+
and same_flags[0] not in actual_flags_processed
|
|
360
|
+
):
|
|
361
|
+
if same_flags[0] in actual_flags_processed:
|
|
362
|
+
actual_flags_processed.append(same_flags[1])
|
|
363
|
+
elif same_flags[1] in actual_flags_processed:
|
|
364
|
+
actual_flags_processed.append(same_flags[0])
|
|
365
|
+
|
|
366
|
+
if len(actual_flags_processed) < len(self.flags_processed):
|
|
367
|
+
self.flags_processed = actual_flags_processed
|
|
368
|
+
if cmd:
|
|
369
|
+
self.options = {
|
|
370
|
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
|
371
|
+
for k in self.original_options.get(cmd).options # type: ignore
|
|
372
|
+
if k not in self.flags_processed
|
|
373
|
+
}
|
|
374
|
+
else:
|
|
375
|
+
self.options = {
|
|
376
|
+
k: self.original_options[k]
|
|
377
|
+
for k in self.original_options
|
|
378
|
+
if k not in self.flags_processed
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
command = self.options.get(cmd)
|
|
382
|
+
options = command.options if command else {} # type: ignore
|
|
383
|
+
command_options = [f"{cmd} {opt}" for opt in options]
|
|
384
|
+
text_list = [text in val for val in command_options]
|
|
385
|
+
if cmd and cmd in self.options and text_list:
|
|
386
|
+
completer = WordCompleter(
|
|
387
|
+
list(self.options.get(cmd).options.keys()), # type: ignore
|
|
388
|
+
ignore_case=self.ignore_case,
|
|
389
|
+
)
|
|
390
|
+
elif bool([val for val in self.options if text in val]):
|
|
391
|
+
completer = WordCompleter(
|
|
392
|
+
list(self.options.keys()), ignore_case=self.ignore_case
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
# The user has delete part of the first command and we need to reset options
|
|
396
|
+
if bool([val for val in self.original_options if text in val]):
|
|
397
|
+
self.options = self.original_options
|
|
398
|
+
self.flags_processed = list()
|
|
399
|
+
completer = WordCompleter(
|
|
400
|
+
list(self.options.keys()), ignore_case=self.ignore_case
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# This is a WordCompleter
|
|
404
|
+
yield from completer.get_completions(document, complete_event)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class CustomFileHistory(FileHistory):
|
|
408
|
+
"""Filtered file history."""
|
|
409
|
+
|
|
410
|
+
def sanitize_input(self, string: str) -> str:
|
|
411
|
+
"""Sanitize sensitive information from the input string by parsing arguments."""
|
|
412
|
+
keywords = ["--password", "--email", "--pat"]
|
|
413
|
+
string_list = string.split(" ")
|
|
414
|
+
|
|
415
|
+
for kw in keywords:
|
|
416
|
+
if kw in string_list:
|
|
417
|
+
index = string_list.index(kw)
|
|
418
|
+
if len(string_list) > index + 1:
|
|
419
|
+
string_list[index + 1] = "********"
|
|
420
|
+
|
|
421
|
+
result = " ".join(string_list)
|
|
422
|
+
return result
|
|
423
|
+
|
|
424
|
+
def store_string(self, string: str) -> None:
|
|
425
|
+
"""Store string in history."""
|
|
426
|
+
string = self.sanitize_input(string)
|
|
427
|
+
super().store_string(string)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from rich import panel
|
|
4
|
+
from rich.console import (
|
|
5
|
+
Console as RichConsole,
|
|
6
|
+
Theme,
|
|
7
|
+
)
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from openbb_cli.config.menu_text import RICH_TAGS
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from openbb_cli.models.settings import Settings
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Console:
|
|
17
|
+
"""Create a rich console to wrap the console print with a Panel."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
settings: "Settings",
|
|
22
|
+
style: Optional[Dict[str, Any]] = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize the ConsoleAndPanel class."""
|
|
25
|
+
self._console = RichConsole(
|
|
26
|
+
theme=Theme(style),
|
|
27
|
+
highlight=False,
|
|
28
|
+
soft_wrap=True,
|
|
29
|
+
)
|
|
30
|
+
self._settings = settings
|
|
31
|
+
self.menu_text = ""
|
|
32
|
+
self.menu_path = ""
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _filter_rich_tags(text):
|
|
36
|
+
"""Filter out rich tags from text."""
|
|
37
|
+
for val in RICH_TAGS:
|
|
38
|
+
text = text.replace(val, "")
|
|
39
|
+
|
|
40
|
+
return text
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _blend_text(
|
|
44
|
+
message: str, color1: Tuple[int, int, int], color2: Tuple[int, int, int]
|
|
45
|
+
) -> Text:
|
|
46
|
+
"""Blend text from one color to another."""
|
|
47
|
+
text = Text(message)
|
|
48
|
+
r1, g1, b1 = color1
|
|
49
|
+
r2, g2, b2 = color2
|
|
50
|
+
dr = r2 - r1
|
|
51
|
+
dg = g2 - g1
|
|
52
|
+
db = b2 - b1
|
|
53
|
+
size = len(text) + 5
|
|
54
|
+
for index in range(size):
|
|
55
|
+
blend = index / size
|
|
56
|
+
color = f"#{int(r1 + dr * blend):02X}{int(g1 + dg * blend):02X}{int(b1 + db * blend):02X}"
|
|
57
|
+
text.stylize(color, index, index + 1)
|
|
58
|
+
return text
|
|
59
|
+
|
|
60
|
+
def print(self, *args, **kwargs):
|
|
61
|
+
"""Print the text to the console."""
|
|
62
|
+
if kwargs and "text" in list(kwargs) and "menu" in list(kwargs):
|
|
63
|
+
if not self._settings.TEST_MODE:
|
|
64
|
+
if self._settings.ENABLE_RICH_PANEL:
|
|
65
|
+
if self._settings.SHOW_VERSION:
|
|
66
|
+
version = self._settings.VERSION
|
|
67
|
+
version = f"[param]OpenBB Platform CLI v{version}[/param] (https://openbb.co)"
|
|
68
|
+
else:
|
|
69
|
+
version = (
|
|
70
|
+
"[param]OpenBB Platform CLI[/param] (https://openbb.co)"
|
|
71
|
+
)
|
|
72
|
+
self._console.print(
|
|
73
|
+
panel.Panel(
|
|
74
|
+
"\n" + kwargs["text"],
|
|
75
|
+
title=kwargs["menu"],
|
|
76
|
+
subtitle_align="right",
|
|
77
|
+
subtitle=version,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
self._console.print(kwargs["text"])
|
|
83
|
+
else:
|
|
84
|
+
print(self._filter_rich_tags(kwargs["text"])) # noqa: T201
|
|
85
|
+
elif not self._settings.TEST_MODE:
|
|
86
|
+
self._console.print(*args, **kwargs)
|
|
87
|
+
else:
|
|
88
|
+
print(*args, **kwargs) # noqa: T201
|
|
89
|
+
|
|
90
|
+
def input(self, *args, **kwargs):
|
|
91
|
+
"""Get input from the user."""
|
|
92
|
+
self.print(*args, **kwargs, end="")
|
|
93
|
+
return input()
|