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.
Files changed (42) hide show
  1. openbb_cli/__init__.py +1 -0
  2. openbb_cli/argparse_translator/__init__.py +0 -0
  3. openbb_cli/argparse_translator/argparse_class_processor.py +148 -0
  4. openbb_cli/argparse_translator/argparse_translator.py +627 -0
  5. openbb_cli/argparse_translator/obbject_registry.py +94 -0
  6. openbb_cli/assets/routines/routine_example.openbb +21 -0
  7. openbb_cli/assets/styles/default/Consolas.ttf +0 -0
  8. openbb_cli/assets/styles/default/dark.mpfstyle.json +47 -0
  9. openbb_cli/assets/styles/default/dark.mplrc.json +7 -0
  10. openbb_cli/assets/styles/default/dark.mplstyle +96 -0
  11. openbb_cli/assets/styles/default/dark.pltstyle.json +132 -0
  12. openbb_cli/assets/styles/default/dark.richstyle.json +9 -0
  13. openbb_cli/assets/styles/default/light.mpfstyle.json +47 -0
  14. openbb_cli/assets/styles/default/light.mplrc.json +7 -0
  15. openbb_cli/assets/styles/default/light.mplstyle +95 -0
  16. openbb_cli/assets/styles/default/light.pltstyle.json +871 -0
  17. openbb_cli/assets/styles/default/light.richstyle.json +9 -0
  18. openbb_cli/assets/styles/default/tables.pltstyle.json +102 -0
  19. openbb_cli/assets/styles/user/openbb.richstyle.json +9 -0
  20. openbb_cli/cli.py +20 -0
  21. openbb_cli/config/__init__.py +1 -0
  22. openbb_cli/config/completer.py +427 -0
  23. openbb_cli/config/console.py +93 -0
  24. openbb_cli/config/constants.py +80 -0
  25. openbb_cli/config/menu_text.py +165 -0
  26. openbb_cli/config/setup.py +11 -0
  27. openbb_cli/config/style.py +108 -0
  28. openbb_cli/controllers/base_controller.py +941 -0
  29. openbb_cli/controllers/base_platform_controller.py +349 -0
  30. openbb_cli/controllers/choices.py +324 -0
  31. openbb_cli/controllers/cli_controller.py +946 -0
  32. openbb_cli/controllers/hub_service.py +107 -0
  33. openbb_cli/controllers/platform_controller_factory.py +58 -0
  34. openbb_cli/controllers/script_parser.py +488 -0
  35. openbb_cli/controllers/settings_controller.py +373 -0
  36. openbb_cli/controllers/utils.py +967 -0
  37. openbb_cli/models/settings.py +73 -0
  38. openbb_cli/session.py +92 -0
  39. openbb_cli-1.0.0.dist-info/METADATA +91 -0
  40. openbb_cli-1.0.0.dist-info/RECORD +42 -0
  41. openbb_cli-1.0.0.dist-info/WHEEL +4 -0
  42. openbb_cli-1.0.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "info": "rgb(224,131,48)",
3
+ "cmds": "rgb(70,156,222)",
4
+ "param": "rgb(247,206,70)",
5
+ "menu": "rgb(50,115,185)",
6
+ "src": "rgb(216,90,64)",
7
+ "unvl": "grey30",
8
+ "help": "#FAC900"
9
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "info": "rgb(224,131,48)",
3
+ "cmds": "#2A7C6E",
4
+ "param": "rgb(247,206,70)",
5
+ "menu": "#427A2E",
6
+ "src": "rgb(216,90,64)",
7
+ "unvl": "grey30",
8
+ "help": "#FAC900"
9
+ }
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()