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,349 @@
|
|
|
1
|
+
"""Platform Equity Controller."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from functools import partial, update_wrapper
|
|
5
|
+
from types import MethodType
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from openbb import obb
|
|
10
|
+
from openbb_charting.core.openbb_figure import OpenBBFigure
|
|
11
|
+
from openbb_cli.argparse_translator.argparse_class_processor import (
|
|
12
|
+
ArgparseClassProcessor,
|
|
13
|
+
)
|
|
14
|
+
from openbb_cli.config.menu_text import MenuText
|
|
15
|
+
from openbb_cli.controllers.base_controller import BaseController
|
|
16
|
+
from openbb_cli.controllers.utils import export_data, print_rich_table
|
|
17
|
+
from openbb_cli.session import Session
|
|
18
|
+
from openbb_core.app.model.obbject import OBBject
|
|
19
|
+
|
|
20
|
+
session = Session()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DummyTranslation:
|
|
24
|
+
"""Dummy Translation for testing."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Construct a Dummy Translation Class."""
|
|
28
|
+
self.paths = {}
|
|
29
|
+
self.translators = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PlatformController(BaseController):
|
|
33
|
+
"""Platform Controller Base class."""
|
|
34
|
+
|
|
35
|
+
CHOICES_GENERATION = True
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
name: str,
|
|
40
|
+
parent_path: List[str],
|
|
41
|
+
platform_target: Optional[type] = None,
|
|
42
|
+
queue: Optional[List[str]] = None,
|
|
43
|
+
translators: Optional[Dict] = None,
|
|
44
|
+
):
|
|
45
|
+
"""Construct a Platform based Controller."""
|
|
46
|
+
self.PATH = f"/{'/'.join(parent_path)}/{name}/" if parent_path else f"/{name}/"
|
|
47
|
+
super().__init__(queue)
|
|
48
|
+
self._name = name
|
|
49
|
+
|
|
50
|
+
if not (platform_target or translators):
|
|
51
|
+
raise ValueError("Either platform_target or translators must be provided.")
|
|
52
|
+
|
|
53
|
+
self._translated_target = (
|
|
54
|
+
ArgparseClassProcessor(
|
|
55
|
+
target_class=platform_target, reference=obb.reference["paths"] # type: ignore
|
|
56
|
+
)
|
|
57
|
+
if platform_target
|
|
58
|
+
else DummyTranslation()
|
|
59
|
+
)
|
|
60
|
+
self.translators = (
|
|
61
|
+
translators
|
|
62
|
+
if translators is not None
|
|
63
|
+
else getattr(self._translated_target, "translators", {})
|
|
64
|
+
)
|
|
65
|
+
self.paths = getattr(self._translated_target, "paths", {})
|
|
66
|
+
|
|
67
|
+
if self.translators:
|
|
68
|
+
self._link_obbject_to_data_processing_commands()
|
|
69
|
+
self._generate_commands()
|
|
70
|
+
self._generate_sub_controllers()
|
|
71
|
+
self.update_completer(self.choices_default)
|
|
72
|
+
|
|
73
|
+
def _link_obbject_to_data_processing_commands(self):
|
|
74
|
+
"""Link data processing commands to OBBject registry."""
|
|
75
|
+
for _, trl in self.translators.items():
|
|
76
|
+
for action in trl._parser._actions: # pylint: disable=protected-access
|
|
77
|
+
if action.dest == "data":
|
|
78
|
+
action.choices = [
|
|
79
|
+
"OBB" + str(i)
|
|
80
|
+
for i in range(len(session.obbject_registry.obbjects))
|
|
81
|
+
]
|
|
82
|
+
action.type = str
|
|
83
|
+
action.nargs = None
|
|
84
|
+
|
|
85
|
+
def _intersect_data_processing_commands(self, ns_parser):
|
|
86
|
+
"""Intersect data processing commands and change the obbject id into an actual obbject."""
|
|
87
|
+
if hasattr(ns_parser, "data"):
|
|
88
|
+
ns_parser.data = int(ns_parser.data.replace("OBB", ""))
|
|
89
|
+
if ns_parser.data in range(len(session.obbject_registry.obbjects)):
|
|
90
|
+
obbject = session.obbject_registry.get(ns_parser.data)
|
|
91
|
+
setattr(ns_parser, "data", obbject.results)
|
|
92
|
+
|
|
93
|
+
return ns_parser
|
|
94
|
+
|
|
95
|
+
def _generate_sub_controllers(self):
|
|
96
|
+
"""Handle paths."""
|
|
97
|
+
for path, value in self.paths.items():
|
|
98
|
+
if value == "path":
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
sub_menu_translators = {}
|
|
102
|
+
choices_commands = []
|
|
103
|
+
|
|
104
|
+
for translator_name, translator in self.translators.items():
|
|
105
|
+
if f"{self._name}_{path}" in translator_name:
|
|
106
|
+
new_name = translator_name.replace(f"{self._name}_{path}_", "")
|
|
107
|
+
sub_menu_translators[new_name] = translator
|
|
108
|
+
choices_commands.append(new_name)
|
|
109
|
+
|
|
110
|
+
if translator_name in self.CHOICES_COMMANDS:
|
|
111
|
+
self.CHOICES_COMMANDS.remove(translator_name)
|
|
112
|
+
|
|
113
|
+
# Create the sub controller as a new class
|
|
114
|
+
class_name = f"{self._name.capitalize()}{path.capitalize()}Controller"
|
|
115
|
+
SubController = type(
|
|
116
|
+
class_name,
|
|
117
|
+
(PlatformController,),
|
|
118
|
+
{
|
|
119
|
+
"CHOICES_GENERATION": True,
|
|
120
|
+
# "CHOICES_MENUS": [],
|
|
121
|
+
"CHOICES_COMMANDS": choices_commands,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._generate_controller_call(
|
|
126
|
+
controller=SubController,
|
|
127
|
+
name=path,
|
|
128
|
+
parent_path=self.path,
|
|
129
|
+
translators=sub_menu_translators,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _generate_commands(self):
|
|
133
|
+
"""Generate commands."""
|
|
134
|
+
for name, translator in self.translators.items():
|
|
135
|
+
# Prepare the translator name to create a command call in the controller
|
|
136
|
+
new_name = name.replace(f"{self._name}_", "")
|
|
137
|
+
|
|
138
|
+
self._generate_command_call(name=new_name, translator=translator)
|
|
139
|
+
|
|
140
|
+
def _generate_command_call(self, name, translator):
|
|
141
|
+
"""Generate command call."""
|
|
142
|
+
|
|
143
|
+
def method(self, other_args: List[str], translator=translator):
|
|
144
|
+
"""Call the translator."""
|
|
145
|
+
parser = translator.parser
|
|
146
|
+
|
|
147
|
+
if ns_parser := self.parse_known_args_and_warn(
|
|
148
|
+
parser=parser,
|
|
149
|
+
other_args=other_args,
|
|
150
|
+
export_allowed="raw_data_and_figures",
|
|
151
|
+
):
|
|
152
|
+
try:
|
|
153
|
+
ns_parser = self._intersect_data_processing_commands(ns_parser)
|
|
154
|
+
|
|
155
|
+
obbject = translator.execute_func(parsed_args=ns_parser)
|
|
156
|
+
df: pd.DataFrame = pd.DataFrame()
|
|
157
|
+
fig: OpenBBFigure = None
|
|
158
|
+
title = f"{self.PATH}{translator.func.__name__}"
|
|
159
|
+
|
|
160
|
+
if obbject:
|
|
161
|
+
|
|
162
|
+
if isinstance(obbject, OBBject):
|
|
163
|
+
if session.max_obbjects_exceeded() and obbject.results:
|
|
164
|
+
session.obbject_registry.remove()
|
|
165
|
+
session.console.print(
|
|
166
|
+
"[yellow]Maximum number of OBBjects reached. The oldest entry was removed.[yellow]"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# use the obbject to store the command so we can display it later on results
|
|
170
|
+
obbject.extra["command"] = f"{title} {' '.join(other_args)}"
|
|
171
|
+
|
|
172
|
+
register_result = session.obbject_registry.register(obbject)
|
|
173
|
+
|
|
174
|
+
# we need to force to re-link so that the new obbject
|
|
175
|
+
# is immediately available for data processing commands
|
|
176
|
+
self._link_obbject_to_data_processing_commands()
|
|
177
|
+
# also update the completer
|
|
178
|
+
self.update_completer(self.choices_default)
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
session.settings.SHOW_MSG_OBBJECT_REGISTRY
|
|
182
|
+
and register_result
|
|
183
|
+
):
|
|
184
|
+
session.console.print(
|
|
185
|
+
"Added `OBBject` to cached results."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# making the dataframe available
|
|
189
|
+
# either for printing or exporting (or both)
|
|
190
|
+
df = obbject.to_dataframe()
|
|
191
|
+
|
|
192
|
+
if hasattr(ns_parser, "chart") and ns_parser.chart:
|
|
193
|
+
obbject.show()
|
|
194
|
+
fig = obbject.chart.fig if obbject.chart else None
|
|
195
|
+
else:
|
|
196
|
+
if isinstance(df.columns, pd.RangeIndex):
|
|
197
|
+
df.columns = [str(i) for i in df.columns]
|
|
198
|
+
|
|
199
|
+
print_rich_table(df=df, show_index=True, title=title)
|
|
200
|
+
|
|
201
|
+
elif isinstance(obbject, dict):
|
|
202
|
+
df = pd.DataFrame.from_dict(obbject, orient="columns")
|
|
203
|
+
print_rich_table(df=df, show_index=True, title=title)
|
|
204
|
+
|
|
205
|
+
elif not isinstance(obbject, OBBject):
|
|
206
|
+
session.console.print(obbject)
|
|
207
|
+
|
|
208
|
+
if (
|
|
209
|
+
hasattr(ns_parser, "export")
|
|
210
|
+
and ns_parser.export
|
|
211
|
+
and not df.empty
|
|
212
|
+
):
|
|
213
|
+
sheet_name = getattr(ns_parser, "sheet_name", None)
|
|
214
|
+
if sheet_name and isinstance(sheet_name, list):
|
|
215
|
+
sheet_name = sheet_name[0]
|
|
216
|
+
|
|
217
|
+
export_data(
|
|
218
|
+
export_type=",".join(ns_parser.export),
|
|
219
|
+
dir_path=os.path.dirname(os.path.abspath(__file__)),
|
|
220
|
+
func_name=translator.func.__name__,
|
|
221
|
+
df=df,
|
|
222
|
+
sheet_name=sheet_name,
|
|
223
|
+
figure=fig,
|
|
224
|
+
)
|
|
225
|
+
elif hasattr(ns_parser, "export") and ns_parser.export and df.empty:
|
|
226
|
+
session.console.print("[yellow]No data to export.[/yellow]")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
session.console.print(f"[red]{e}[/]\n")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
# Bind the method to the class
|
|
233
|
+
bound_method = MethodType(method, self)
|
|
234
|
+
|
|
235
|
+
# Update the wrapper and set the attribute
|
|
236
|
+
bound_method = update_wrapper( # type: ignore
|
|
237
|
+
partial(bound_method, translator=translator), method
|
|
238
|
+
)
|
|
239
|
+
setattr(self, f"call_{name}", bound_method)
|
|
240
|
+
|
|
241
|
+
def _generate_controller_call(self, controller, name, parent_path, translators):
|
|
242
|
+
"""Generate controller call."""
|
|
243
|
+
|
|
244
|
+
def method(self, _, controller, name, parent_path, translators):
|
|
245
|
+
"""Call the controller."""
|
|
246
|
+
self.queue = self.load_class(
|
|
247
|
+
class_ins=controller,
|
|
248
|
+
name=name,
|
|
249
|
+
parent_path=parent_path,
|
|
250
|
+
translators=translators,
|
|
251
|
+
queue=self.queue,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Bind the method to the class
|
|
255
|
+
bound_method = MethodType(method, self)
|
|
256
|
+
|
|
257
|
+
# Update the wrapper and set the attribute
|
|
258
|
+
bound_method = update_wrapper( # type: ignore
|
|
259
|
+
partial(
|
|
260
|
+
bound_method,
|
|
261
|
+
name=name,
|
|
262
|
+
parent_path=parent_path,
|
|
263
|
+
translators=translators,
|
|
264
|
+
controller=controller,
|
|
265
|
+
),
|
|
266
|
+
method,
|
|
267
|
+
)
|
|
268
|
+
setattr(self, f"call_{name}", bound_method)
|
|
269
|
+
|
|
270
|
+
def _get_command_description(self, command: str) -> str:
|
|
271
|
+
"""Get command description."""
|
|
272
|
+
command_description = (
|
|
273
|
+
obb.reference["paths"] # type: ignore
|
|
274
|
+
.get(f"{self.PATH}{command}", {})
|
|
275
|
+
.get("description", "")
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if not command_description:
|
|
279
|
+
trl = self.translators.get(
|
|
280
|
+
f"{self._name}_{command}"
|
|
281
|
+
) or self.translators.get(command)
|
|
282
|
+
if trl and hasattr(trl, "parser"):
|
|
283
|
+
command_description = trl.parser.description
|
|
284
|
+
|
|
285
|
+
return command_description.split(".")[0].lower()
|
|
286
|
+
|
|
287
|
+
def _get_menu_description(self, menu: str) -> str:
|
|
288
|
+
"""Get menu description."""
|
|
289
|
+
|
|
290
|
+
def _get_sub_menu_commands():
|
|
291
|
+
"""Get sub menu commands."""
|
|
292
|
+
sub_path = f"{self.PATH[1:].replace('/','_')}{menu}"
|
|
293
|
+
commands = []
|
|
294
|
+
for trl in self.translators:
|
|
295
|
+
if sub_path in trl:
|
|
296
|
+
commands.append(trl.replace(f"{sub_path}_", ""))
|
|
297
|
+
return commands
|
|
298
|
+
|
|
299
|
+
menu_description = (
|
|
300
|
+
obb.reference["routers"] # type: ignore
|
|
301
|
+
.get(f"{self.PATH}{menu}", {})
|
|
302
|
+
.get("description", "")
|
|
303
|
+
) or ""
|
|
304
|
+
if menu_description:
|
|
305
|
+
return menu_description.split(".")[0].lower()
|
|
306
|
+
|
|
307
|
+
# If no description is found, return the sub menu commands
|
|
308
|
+
return ", ".join(_get_sub_menu_commands())
|
|
309
|
+
|
|
310
|
+
def print_help(self):
|
|
311
|
+
"""Print help."""
|
|
312
|
+
mt = MenuText(self.PATH)
|
|
313
|
+
|
|
314
|
+
if self.CHOICES_MENUS:
|
|
315
|
+
for menu in self.CHOICES_MENUS:
|
|
316
|
+
description = self._get_menu_description(menu)
|
|
317
|
+
mt.add_menu(name=menu, description=description)
|
|
318
|
+
|
|
319
|
+
if self.CHOICES_COMMANDS:
|
|
320
|
+
mt.add_raw("\n")
|
|
321
|
+
|
|
322
|
+
if self.CHOICES_COMMANDS:
|
|
323
|
+
for command in self.CHOICES_COMMANDS:
|
|
324
|
+
command_description = self._get_command_description(command)
|
|
325
|
+
mt.add_cmd(
|
|
326
|
+
name=command.replace(f"{self._name}_", ""),
|
|
327
|
+
description=command_description,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if session.obbject_registry.obbjects:
|
|
331
|
+
mt.add_info("\nCached Results")
|
|
332
|
+
for key, value in list(session.obbject_registry.all.items())[
|
|
333
|
+
: session.settings.N_TO_DISPLAY_OBBJECT_REGISTRY
|
|
334
|
+
]:
|
|
335
|
+
mt.add_raw(
|
|
336
|
+
f"[yellow]OBB{key}[/yellow]: {value['command']}",
|
|
337
|
+
left_spacing=True,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
session.console.print(text=mt.menu_text, menu=self.PATH)
|
|
341
|
+
|
|
342
|
+
settings = session.settings
|
|
343
|
+
dev_mode = settings.DEBUG_MODE or settings.TEST_MODE
|
|
344
|
+
if mt.warnings and dev_mode:
|
|
345
|
+
session.console.print("")
|
|
346
|
+
for w in mt.warnings:
|
|
347
|
+
w_str = str(w).replace("{", "").replace("}", "").replace("'", "")
|
|
348
|
+
session.console.print(f"[yellow]{w_str}[/yellow]")
|
|
349
|
+
session.console.print("")
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""This module contains functions to build the choice map for the controllers."""
|
|
2
|
+
|
|
3
|
+
from argparse import SUPPRESS, ArgumentParser
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from inspect import isfunction, unwrap
|
|
6
|
+
from types import MethodType
|
|
7
|
+
from typing import Callable, List, Literal
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
from openbb_cli.controllers.utils import (
|
|
11
|
+
check_file_type_saved,
|
|
12
|
+
check_positive,
|
|
13
|
+
)
|
|
14
|
+
from openbb_cli.session import Session
|
|
15
|
+
|
|
16
|
+
session = Session()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __mock_parse_known_args_and_warn(
|
|
20
|
+
controller, # pylint: disable=unused-argument
|
|
21
|
+
parser: ArgumentParser,
|
|
22
|
+
other_args: List[str],
|
|
23
|
+
export_allowed: Literal[
|
|
24
|
+
"no_export", "raw_data_only", "figures_only", "raw_data_and_figures"
|
|
25
|
+
] = "no_export",
|
|
26
|
+
raw: bool = False,
|
|
27
|
+
limit: int = 0,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Add arguments.
|
|
30
|
+
|
|
31
|
+
Add the arguments that would have normally added by :
|
|
32
|
+
- openbb_cli.base_controller.BaseController.parse_known_args_and_warn
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
parser: argparse.ArgumentParser
|
|
37
|
+
Parser with predefined arguments
|
|
38
|
+
other_args: List[str]
|
|
39
|
+
list of arguments to parse
|
|
40
|
+
export_allowed: Literal["no_export", "raw_data_only", "figures_only", "raw_data_and_figures"]
|
|
41
|
+
Export options
|
|
42
|
+
raw: bool
|
|
43
|
+
Add the --raw flag
|
|
44
|
+
limit: int
|
|
45
|
+
Add a --limit flag with this number default
|
|
46
|
+
"""
|
|
47
|
+
_ = other_args
|
|
48
|
+
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-h", "--help", action="store_true", help="show this help message"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if export_allowed != "no_export":
|
|
54
|
+
choices_export = []
|
|
55
|
+
help_export = "Does not export!"
|
|
56
|
+
|
|
57
|
+
if export_allowed == "raw_data_only":
|
|
58
|
+
choices_export = ["csv", "json", "xlsx"]
|
|
59
|
+
help_export = "Export raw data into csv, json, xlsx"
|
|
60
|
+
elif export_allowed == "figures_only":
|
|
61
|
+
choices_export = ["png", "jpg", "pdf", "svg"]
|
|
62
|
+
help_export = "Export figure into png, jpg, pdf, svg "
|
|
63
|
+
else:
|
|
64
|
+
choices_export = ["csv", "json", "xlsx", "png", "jpg", "pdf", "svg"]
|
|
65
|
+
help_export = "Export raw data into csv, json, xlsx and figure into png, jpg, pdf, svg "
|
|
66
|
+
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--export",
|
|
69
|
+
default="",
|
|
70
|
+
type=check_file_type_saved(choices_export),
|
|
71
|
+
dest="export",
|
|
72
|
+
help=help_export,
|
|
73
|
+
choices=choices_export,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if raw:
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--raw",
|
|
79
|
+
dest="raw",
|
|
80
|
+
action="store_true",
|
|
81
|
+
default=False,
|
|
82
|
+
help="Flag to display raw data",
|
|
83
|
+
)
|
|
84
|
+
if limit > 0:
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"-l",
|
|
87
|
+
"--limit",
|
|
88
|
+
dest="limit",
|
|
89
|
+
default=limit,
|
|
90
|
+
help="Number of entries to show in data.",
|
|
91
|
+
type=check_positive,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> None:
|
|
96
|
+
"""Add arguments.
|
|
97
|
+
|
|
98
|
+
Add the arguments that would have normally added by:
|
|
99
|
+
- openbb_cli.parent_classes.BaseController.parse_simple_args
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
parser: argparse.ArgumentParser
|
|
104
|
+
Parser with predefined arguments
|
|
105
|
+
other_args: List[str]
|
|
106
|
+
List of arguments to parse
|
|
107
|
+
"""
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"-h", "--help", action="store_true", help="show this help message"
|
|
110
|
+
)
|
|
111
|
+
_ = other_args
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def __get_command_func(controller, command: str):
|
|
115
|
+
"""Get the function with the name `f"call_{command}"` from controller object.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
controller: BaseController
|
|
120
|
+
Instance of the CLI Controller.
|
|
121
|
+
command: str
|
|
122
|
+
A name from controller.CHOICES_COMMANDS
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
Callable: Command function.
|
|
127
|
+
"""
|
|
128
|
+
if command not in controller.CHOICES_COMMANDS:
|
|
129
|
+
raise AttributeError(
|
|
130
|
+
f"The following command is not inside `CHOICES_COMMANDS` : '{command}'"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
command = f"call_{command}"
|
|
134
|
+
command_func = getattr(controller, command)
|
|
135
|
+
command_func = unwrap(func=command_func)
|
|
136
|
+
|
|
137
|
+
if isfunction(command_func):
|
|
138
|
+
command_func = MethodType(command_func, controller)
|
|
139
|
+
|
|
140
|
+
return command_func
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def contains_functions_to_patch(command_func: Callable) -> bool:
|
|
144
|
+
"""Check command function.
|
|
145
|
+
|
|
146
|
+
Check if a `command_func` actually contains the functions we want to mock, i.e.:
|
|
147
|
+
- parse_simple_args
|
|
148
|
+
- parse_known_args_and_warn
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
command_func: Callable
|
|
153
|
+
Function to check.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
bool: Whether or not `command_func` contains the mocked functions.
|
|
158
|
+
"""
|
|
159
|
+
co_names = command_func.__code__.co_names
|
|
160
|
+
|
|
161
|
+
return bool(
|
|
162
|
+
"parse_simple_args" in co_names or "parse_known_args_and_warn" in co_names
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@contextmanager
|
|
167
|
+
def __patch_controller_functions(controller):
|
|
168
|
+
"""Patch controller functions.
|
|
169
|
+
|
|
170
|
+
Patch the following function from a BaseController instance:
|
|
171
|
+
- parse_simple_args
|
|
172
|
+
- parse_known_args_and_warn
|
|
173
|
+
|
|
174
|
+
These functions take an 'argparse.ArgumentParser' object as parameter.
|
|
175
|
+
We want to intercept this 'argparse.ArgumentParser' object.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
controller: BaseController
|
|
180
|
+
BaseController object that needs to be patched.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
List[Callable]: List of mocked functions.
|
|
185
|
+
"""
|
|
186
|
+
bound_mock_parse_known_args_and_warn = MethodType(
|
|
187
|
+
__mock_parse_known_args_and_warn,
|
|
188
|
+
controller,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
rich = patch(
|
|
192
|
+
target="openbb_cli.config.console.Console.print",
|
|
193
|
+
return_value=None,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
patcher_list = [
|
|
197
|
+
patch.object(
|
|
198
|
+
target=controller,
|
|
199
|
+
attribute="parse_simple_args",
|
|
200
|
+
side_effect=__mock_parse_simple_args,
|
|
201
|
+
return_value=None,
|
|
202
|
+
),
|
|
203
|
+
patch.object(
|
|
204
|
+
target=controller,
|
|
205
|
+
attribute="parse_known_args_and_warn",
|
|
206
|
+
side_effect=bound_mock_parse_known_args_and_warn,
|
|
207
|
+
return_value=None,
|
|
208
|
+
),
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
if not session.settings.DEBUG_MODE:
|
|
212
|
+
rich.start()
|
|
213
|
+
patched_function_list = []
|
|
214
|
+
for patcher in patcher_list:
|
|
215
|
+
patched_function_list.append(patcher.start())
|
|
216
|
+
|
|
217
|
+
yield patched_function_list
|
|
218
|
+
|
|
219
|
+
if not session.settings.DEBUG_MODE:
|
|
220
|
+
rich.stop()
|
|
221
|
+
for patcher in patcher_list:
|
|
222
|
+
patcher.stop()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _get_argument_parser(
|
|
226
|
+
controller,
|
|
227
|
+
command: str,
|
|
228
|
+
) -> ArgumentParser:
|
|
229
|
+
"""Intercept the ArgumentParser instance from the command function.
|
|
230
|
+
|
|
231
|
+
A command function being a function starting with `call_`, like:
|
|
232
|
+
- call_help
|
|
233
|
+
- call_overview
|
|
234
|
+
- call_load
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
controller: BaseController
|
|
239
|
+
Instance of the CLI Controller.
|
|
240
|
+
command: str
|
|
241
|
+
A name from `controller.CHOICES_COMMANDS`.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
ArgumentParser: ArgumentParser instance from the command function.
|
|
246
|
+
"""
|
|
247
|
+
command_func: Callable = __get_command_func(controller=controller, command=command)
|
|
248
|
+
|
|
249
|
+
if not contains_functions_to_patch(command_func=command_func):
|
|
250
|
+
raise AssertionError(
|
|
251
|
+
f"One of these functions should be inside `call_{command}`:\n"
|
|
252
|
+
" - parse_simple_args\n"
|
|
253
|
+
" - parse_known_args_and_warn\n"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
with __patch_controller_functions(controller=controller) as patched_function_list:
|
|
257
|
+
command_func([])
|
|
258
|
+
|
|
259
|
+
call_count = 0
|
|
260
|
+
for patched_function in patched_function_list:
|
|
261
|
+
call_count += patched_function.call_count
|
|
262
|
+
if patched_function.call_count == 1:
|
|
263
|
+
args, kwargs = patched_function.call_args
|
|
264
|
+
argument_parser = (
|
|
265
|
+
kwargs["parser"] if kwargs.get("parser", None) else args[0]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if call_count != 1:
|
|
269
|
+
raise AssertionError(
|
|
270
|
+
f"One of these functions should be called once inside `call_{command}`:\n"
|
|
271
|
+
" - parse_simple_args\n"
|
|
272
|
+
" - parse_known_args_and_warn\n"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return argument_parser
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _build_command_choice_map(argument_parser: ArgumentParser) -> dict:
|
|
279
|
+
"""Build the choice map for a command."""
|
|
280
|
+
choice_map: dict = {}
|
|
281
|
+
for action in argument_parser._actions: # pylint: disable=protected-access
|
|
282
|
+
if action.help == SUPPRESS:
|
|
283
|
+
continue
|
|
284
|
+
if len(action.option_strings) == 1:
|
|
285
|
+
long_name = action.option_strings[0]
|
|
286
|
+
short_name = ""
|
|
287
|
+
elif len(action.option_strings) == 2:
|
|
288
|
+
short_name = action.option_strings[0]
|
|
289
|
+
long_name = action.option_strings[1]
|
|
290
|
+
else:
|
|
291
|
+
raise AttributeError(f"Invalid argument_parser: {argument_parser}")
|
|
292
|
+
|
|
293
|
+
if hasattr(action, "choices") and action.choices:
|
|
294
|
+
choice_map[long_name] = {str(c): {} for c in action.choices}
|
|
295
|
+
else:
|
|
296
|
+
choice_map[long_name] = {}
|
|
297
|
+
|
|
298
|
+
if short_name and long_name:
|
|
299
|
+
choice_map[short_name] = long_name
|
|
300
|
+
|
|
301
|
+
return choice_map
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def build_controller_choice_map(controller) -> dict:
|
|
305
|
+
"""Build the choice map for a controller."""
|
|
306
|
+
command_list = controller.CHOICES_COMMANDS
|
|
307
|
+
controller_choice_map: dict = {c: {} for c in controller.controller_choices}
|
|
308
|
+
|
|
309
|
+
for command in command_list:
|
|
310
|
+
try:
|
|
311
|
+
argument_parser = _get_argument_parser(
|
|
312
|
+
controller=controller,
|
|
313
|
+
command=command,
|
|
314
|
+
)
|
|
315
|
+
controller_choice_map[command] = _build_command_choice_map(
|
|
316
|
+
argument_parser=argument_parser
|
|
317
|
+
)
|
|
318
|
+
except Exception as exception:
|
|
319
|
+
if session.settings.DEBUG_MODE:
|
|
320
|
+
raise Exception(
|
|
321
|
+
f"On command : `{command}`.\n{str(exception)}"
|
|
322
|
+
) from exception
|
|
323
|
+
|
|
324
|
+
return controller_choice_map
|