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,627 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
import re
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Callable,
|
|
9
|
+
Dict,
|
|
10
|
+
List,
|
|
11
|
+
Literal,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
get_args,
|
|
17
|
+
get_origin,
|
|
18
|
+
get_type_hints,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from openbb_core.app.model.field import OpenBBField
|
|
22
|
+
from pydantic import BaseModel, model_validator
|
|
23
|
+
from typing_extensions import Annotated
|
|
24
|
+
|
|
25
|
+
# pylint: disable=protected-access
|
|
26
|
+
|
|
27
|
+
SEP = "__"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ArgparseActionType(Enum):
|
|
31
|
+
store = "store"
|
|
32
|
+
store_true = "store_true"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CustomArgument(BaseModel):
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
type: Optional[Any]
|
|
39
|
+
dest: str
|
|
40
|
+
default: Any
|
|
41
|
+
required: bool
|
|
42
|
+
action: Literal["store_true", "store"]
|
|
43
|
+
help: str
|
|
44
|
+
nargs: Optional[Literal["+"]]
|
|
45
|
+
choices: Optional[Tuple]
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after") # type: ignore
|
|
48
|
+
@classmethod
|
|
49
|
+
def validate_action(cls, values: "CustomArgument"):
|
|
50
|
+
if values.type is bool and values.action != "store_true":
|
|
51
|
+
raise ValueError('If type is bool, action must be "store_true"')
|
|
52
|
+
return values
|
|
53
|
+
|
|
54
|
+
@model_validator(mode="after") # type: ignore
|
|
55
|
+
@classmethod
|
|
56
|
+
def remove_props_on_store_true(cls, values: "CustomArgument"):
|
|
57
|
+
if values.action == "store_true":
|
|
58
|
+
values.type = None
|
|
59
|
+
values.nargs = None
|
|
60
|
+
values.choices = None
|
|
61
|
+
return values
|
|
62
|
+
|
|
63
|
+
# override
|
|
64
|
+
def model_dump(self, **kwargs):
|
|
65
|
+
|
|
66
|
+
res = super().model_dump(**kwargs)
|
|
67
|
+
|
|
68
|
+
# Check if choices is present and if it's an empty tuple remove it
|
|
69
|
+
if "choices" in res and not res["choices"]:
|
|
70
|
+
del res["choices"]
|
|
71
|
+
|
|
72
|
+
return res
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CustomArgumentGroup(BaseModel):
|
|
76
|
+
name: str
|
|
77
|
+
arguments: List[CustomArgument]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ReferenceToCustomArgumentsProcessor:
|
|
81
|
+
def __init__(self, reference: Dict[str, Dict]):
|
|
82
|
+
"""Initializes the ReferenceToCustomArgumentsProcessor."""
|
|
83
|
+
self.reference = reference
|
|
84
|
+
self.custom_groups: Dict[str, List[CustomArgumentGroup]] = {}
|
|
85
|
+
|
|
86
|
+
self.build_custom_groups()
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _make_type_parsable(type_: str) -> type:
|
|
90
|
+
"""Make the type parsable by removing the annotations."""
|
|
91
|
+
if "Union" in type_ and "str" in type_:
|
|
92
|
+
return str
|
|
93
|
+
if "Union" in type_ and "int" in type_:
|
|
94
|
+
return int
|
|
95
|
+
if type_ in ["date", "datetime.time", "time"]:
|
|
96
|
+
return str
|
|
97
|
+
|
|
98
|
+
if any(x in type_ for x in ["gt=", "ge=", "lt=", "le="]):
|
|
99
|
+
if "Annotated" in type_:
|
|
100
|
+
type_ = type_.replace("Annotated[", "").replace("]", "")
|
|
101
|
+
type_ = type_.split(",")[0]
|
|
102
|
+
|
|
103
|
+
return eval(type_) # noqa: S307, E501 pylint: disable=eval-used
|
|
104
|
+
|
|
105
|
+
def _parse_type(self, type_: str) -> type:
|
|
106
|
+
"""Parse the type from the string representation."""
|
|
107
|
+
type_ = self._make_type_parsable(type_) # type: ignore
|
|
108
|
+
|
|
109
|
+
if get_origin(type_) is Literal:
|
|
110
|
+
type_ = type(get_args(type_)[0]) # type: ignore
|
|
111
|
+
|
|
112
|
+
return type_ # type: ignore
|
|
113
|
+
|
|
114
|
+
def _get_nargs(self, type_: type) -> Optional[Union[int, str]]:
|
|
115
|
+
"""Get the nargs for the given type."""
|
|
116
|
+
if get_origin(type_) is list:
|
|
117
|
+
return "+"
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def _get_choices(self, type_: str, custom_choices: Any) -> Tuple:
|
|
121
|
+
"""Get the choices for the given type."""
|
|
122
|
+
type_ = self._make_type_parsable(type_) # type: ignore
|
|
123
|
+
type_origin = get_origin(type_)
|
|
124
|
+
|
|
125
|
+
choices = ()
|
|
126
|
+
|
|
127
|
+
if type_origin is Literal:
|
|
128
|
+
choices = get_args(type_)
|
|
129
|
+
|
|
130
|
+
if type_origin is list:
|
|
131
|
+
type_ = get_args(type_)[0]
|
|
132
|
+
|
|
133
|
+
if get_origin(type_) is Literal:
|
|
134
|
+
choices = get_args(type_)
|
|
135
|
+
|
|
136
|
+
if type_origin is Union and type(None) in get_args(type_):
|
|
137
|
+
# remove NoneType from the args
|
|
138
|
+
args = [arg for arg in get_args(type_) if arg != type(None)]
|
|
139
|
+
# if there is only one arg left, use it
|
|
140
|
+
if len(args) > 1:
|
|
141
|
+
raise ValueError("Union with NoneType should have only one type left")
|
|
142
|
+
type_ = args[0]
|
|
143
|
+
|
|
144
|
+
if get_origin(type_) is Literal:
|
|
145
|
+
choices = get_args(type_)
|
|
146
|
+
|
|
147
|
+
if custom_choices:
|
|
148
|
+
return tuple(custom_choices)
|
|
149
|
+
|
|
150
|
+
return choices
|
|
151
|
+
|
|
152
|
+
def build_custom_groups(self):
|
|
153
|
+
"""Build the custom groups from the reference."""
|
|
154
|
+
for route, v in self.reference.items():
|
|
155
|
+
|
|
156
|
+
for provider, args in v["parameters"].items():
|
|
157
|
+
if provider == "standard":
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
custom_arguments = []
|
|
161
|
+
for arg in args:
|
|
162
|
+
if arg.get("standard"):
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
type_ = self._parse_type(arg["type"])
|
|
166
|
+
|
|
167
|
+
custom_arguments.append(
|
|
168
|
+
CustomArgument(
|
|
169
|
+
name=arg["name"],
|
|
170
|
+
type=type_,
|
|
171
|
+
dest=arg["name"],
|
|
172
|
+
default=arg["default"],
|
|
173
|
+
required=not (arg["optional"]),
|
|
174
|
+
action="store" if type_ != bool else "store_true",
|
|
175
|
+
help=arg["description"],
|
|
176
|
+
nargs=self._get_nargs(type_), # type: ignore
|
|
177
|
+
choices=self._get_choices(
|
|
178
|
+
arg["type"], custom_choices=arg["choices"]
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
group = CustomArgumentGroup(name=provider, arguments=custom_arguments)
|
|
184
|
+
|
|
185
|
+
if route not in self.custom_groups:
|
|
186
|
+
self.custom_groups[route] = []
|
|
187
|
+
|
|
188
|
+
self.custom_groups[route].append(group)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ArgparseTranslator:
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
func: Callable,
|
|
195
|
+
custom_argument_groups: Optional[List[CustomArgumentGroup]] = None,
|
|
196
|
+
add_help: Optional[bool] = True,
|
|
197
|
+
):
|
|
198
|
+
"""
|
|
199
|
+
Initializes the ArgparseTranslator.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
func (Callable): The function to translate into an argparse program.
|
|
203
|
+
add_help (Optional[bool], optional): Whether to add the help argument. Defaults to False.
|
|
204
|
+
"""
|
|
205
|
+
self.func = func
|
|
206
|
+
self.signature = inspect.signature(func)
|
|
207
|
+
self.type_hints = get_type_hints(func)
|
|
208
|
+
self.provider_parameters: Dict[str, List[str]] = {}
|
|
209
|
+
|
|
210
|
+
self._parser = argparse.ArgumentParser(
|
|
211
|
+
prog=func.__name__,
|
|
212
|
+
description=self._build_description(func.__doc__), # type: ignore
|
|
213
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
214
|
+
add_help=add_help if add_help else False,
|
|
215
|
+
)
|
|
216
|
+
self._required = self._parser.add_argument_group("required arguments")
|
|
217
|
+
|
|
218
|
+
if any(param in self.type_hints for param in self.signature.parameters):
|
|
219
|
+
self._generate_argparse_arguments(self.signature.parameters)
|
|
220
|
+
|
|
221
|
+
if custom_argument_groups:
|
|
222
|
+
for group in custom_argument_groups:
|
|
223
|
+
self.provider_parameters[group.name] = []
|
|
224
|
+
argparse_group = self._parser.add_argument_group(group.name)
|
|
225
|
+
for argument in group.arguments:
|
|
226
|
+
self._handle_argument_in_groups(argument, argparse_group)
|
|
227
|
+
|
|
228
|
+
def _handle_argument_in_groups(self, argument, group):
|
|
229
|
+
"""Handle the argument and add it to the parser."""
|
|
230
|
+
|
|
231
|
+
def _in_group(arg, group_title):
|
|
232
|
+
for action_group in self._parser._action_groups:
|
|
233
|
+
if action_group.title == group_title:
|
|
234
|
+
for action in action_group._group_actions:
|
|
235
|
+
opts = action.option_strings
|
|
236
|
+
if (opts and opts[0] == arg) or action.dest == arg:
|
|
237
|
+
return True
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
def _remove_argument(arg) -> List[Optional[str]]:
|
|
241
|
+
groups_w_arg = []
|
|
242
|
+
|
|
243
|
+
# remove the argument from the parser
|
|
244
|
+
for action in self._parser._actions:
|
|
245
|
+
opts = action.option_strings
|
|
246
|
+
if (opts and opts[0] == arg) or action.dest == arg:
|
|
247
|
+
self._parser._remove_action(action)
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
# remove from all groups
|
|
251
|
+
for action_group in self._parser._action_groups:
|
|
252
|
+
for action in action_group._group_actions:
|
|
253
|
+
opts = action.option_strings
|
|
254
|
+
if (opts and opts[0] == arg) or action.dest == arg:
|
|
255
|
+
action_group._group_actions.remove(action)
|
|
256
|
+
groups_w_arg.append(action_group.title)
|
|
257
|
+
|
|
258
|
+
# remove from _action_groups dict
|
|
259
|
+
self._parser._option_string_actions.pop(f"--{arg}", None)
|
|
260
|
+
|
|
261
|
+
return groups_w_arg
|
|
262
|
+
|
|
263
|
+
def _get_arg_choices(arg) -> Tuple:
|
|
264
|
+
for action in self._parser._actions:
|
|
265
|
+
opts = action.option_strings
|
|
266
|
+
if (opts and opts[0] == arg) or action.dest == arg:
|
|
267
|
+
return tuple(action.choices or ())
|
|
268
|
+
return ()
|
|
269
|
+
|
|
270
|
+
def _update_providers(
|
|
271
|
+
input_string: str, new_provider: List[Optional[str]]
|
|
272
|
+
) -> str:
|
|
273
|
+
pattern = r"\(provider:\s*(.*?)\)"
|
|
274
|
+
providers = re.findall(pattern, input_string)
|
|
275
|
+
providers.extend(new_provider)
|
|
276
|
+
# remove pattern from help and add with new providers
|
|
277
|
+
input_string = re.sub(pattern, "", input_string).strip()
|
|
278
|
+
return f"{input_string} (provider: {', '.join(providers)})"
|
|
279
|
+
|
|
280
|
+
# check if the argument is already in use, if not, add it
|
|
281
|
+
if f"--{argument.name}" not in self._parser._option_string_actions:
|
|
282
|
+
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
|
|
283
|
+
group.add_argument(f"--{argument.name}", **kwargs)
|
|
284
|
+
if group.title in self.provider_parameters:
|
|
285
|
+
self.provider_parameters[group.title].append(argument.name)
|
|
286
|
+
|
|
287
|
+
else:
|
|
288
|
+
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
|
|
289
|
+
model_choices = kwargs.get("choices", ()) or ()
|
|
290
|
+
# extend choices
|
|
291
|
+
choices = tuple(set(_get_arg_choices(argument.name) + model_choices))
|
|
292
|
+
|
|
293
|
+
# check if the argument is in the required arguments
|
|
294
|
+
if _in_group(argument.name, group_title="required arguments"):
|
|
295
|
+
for action in self._required._group_actions:
|
|
296
|
+
if action.dest == argument.name and choices:
|
|
297
|
+
# update choices
|
|
298
|
+
action.choices = choices
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# check if the argument is in the optional arguments
|
|
302
|
+
if _in_group(argument.name, group_title="optional arguments"):
|
|
303
|
+
for action in self._parser._actions:
|
|
304
|
+
if action.dest == argument.name:
|
|
305
|
+
# update choices
|
|
306
|
+
if choices:
|
|
307
|
+
action.choices = choices
|
|
308
|
+
if argument.name not in self.signature.parameters:
|
|
309
|
+
# update help
|
|
310
|
+
action.help = _update_providers(
|
|
311
|
+
action.help or "", [group.title]
|
|
312
|
+
)
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
# if the argument is in use, remove it from all groups
|
|
316
|
+
# and return the groups that had the argument
|
|
317
|
+
groups_w_arg = _remove_argument(argument.name)
|
|
318
|
+
groups_w_arg.append(group.title) # add current group
|
|
319
|
+
|
|
320
|
+
# add it to the optional arguments group instead
|
|
321
|
+
if choices:
|
|
322
|
+
kwargs["choices"] = choices # update choices
|
|
323
|
+
# add provider info to the help
|
|
324
|
+
kwargs["help"] = _update_providers(argument.help or "", groups_w_arg)
|
|
325
|
+
self._parser.add_argument(f"--{argument.name}", **kwargs)
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def parser(self) -> argparse.ArgumentParser:
|
|
329
|
+
return deepcopy(self._parser)
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _build_description(func_doc: str) -> str:
|
|
333
|
+
"""Builds the description of the argparse program from the function docstring."""
|
|
334
|
+
|
|
335
|
+
patterns = ["openbb\n ======", "Parameters\n ----------"]
|
|
336
|
+
|
|
337
|
+
if func_doc:
|
|
338
|
+
for pattern in patterns:
|
|
339
|
+
if pattern in func_doc:
|
|
340
|
+
func_doc = func_doc[: func_doc.index(pattern)].strip()
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
return func_doc
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def _param_is_default(param: inspect.Parameter) -> bool:
|
|
347
|
+
"""Returns True if the parameter has a default value."""
|
|
348
|
+
return param.default != inspect.Parameter.empty
|
|
349
|
+
|
|
350
|
+
def _get_action_type(self, param: inspect.Parameter) -> str:
|
|
351
|
+
"""Returns the argparse action type for the given parameter."""
|
|
352
|
+
param_type = self.type_hints[param.name]
|
|
353
|
+
|
|
354
|
+
if param_type == bool:
|
|
355
|
+
return ArgparseActionType.store_true.value
|
|
356
|
+
return ArgparseActionType.store.value
|
|
357
|
+
|
|
358
|
+
def _get_type_and_choices(
|
|
359
|
+
self, param: inspect.Parameter
|
|
360
|
+
) -> Tuple[Type[Any], Tuple[Any, ...]]:
|
|
361
|
+
"""Returns the type and choices for the given parameter."""
|
|
362
|
+
param_type = self.type_hints[param.name]
|
|
363
|
+
type_origin = get_origin(param_type)
|
|
364
|
+
|
|
365
|
+
choices = ()
|
|
366
|
+
|
|
367
|
+
if type_origin is Literal:
|
|
368
|
+
choices = get_args(param_type)
|
|
369
|
+
param_type = type(choices[0]) # type: ignore
|
|
370
|
+
|
|
371
|
+
if type_origin is list: # TODO: dict should also go here
|
|
372
|
+
param_type = get_args(param_type)[0]
|
|
373
|
+
|
|
374
|
+
if get_origin(param_type) is Literal:
|
|
375
|
+
choices = get_args(param_type)
|
|
376
|
+
param_type = type(choices[0]) # type: ignore
|
|
377
|
+
|
|
378
|
+
if type_origin is Union:
|
|
379
|
+
union_args = get_args(param_type)
|
|
380
|
+
if str in union_args:
|
|
381
|
+
param_type = str
|
|
382
|
+
|
|
383
|
+
# check if it's an Optional, which would be a Union with NoneType
|
|
384
|
+
if type(None) in get_args(param_type):
|
|
385
|
+
# remove NoneType from the args
|
|
386
|
+
args = [arg for arg in get_args(param_type) if arg != type(None)]
|
|
387
|
+
# if there is only one arg left, use it
|
|
388
|
+
if len(args) > 1:
|
|
389
|
+
raise ValueError(
|
|
390
|
+
"Union with NoneType should have only one type left"
|
|
391
|
+
)
|
|
392
|
+
param_type = args[0]
|
|
393
|
+
|
|
394
|
+
if get_origin(param_type) is Literal:
|
|
395
|
+
choices = get_args(param_type)
|
|
396
|
+
param_type = type(choices[0]) # type: ignore
|
|
397
|
+
|
|
398
|
+
# if there are custom choices, override
|
|
399
|
+
choices = self._get_argument_custom_choices(param) or choices # type: ignore
|
|
400
|
+
|
|
401
|
+
return param_type, choices
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def _split_annotation(
|
|
405
|
+
base_annotation: Type[Any], custom_annotation_type: Type
|
|
406
|
+
) -> Tuple[Type[Any], List[Any]]:
|
|
407
|
+
"""Find the base annotation and the custom annotations, namely the OpenBBField."""
|
|
408
|
+
if get_origin(base_annotation) is not Annotated:
|
|
409
|
+
return base_annotation, []
|
|
410
|
+
base_annotation, *maybe_custom_annotations = get_args(base_annotation)
|
|
411
|
+
return base_annotation, [
|
|
412
|
+
annotation
|
|
413
|
+
for annotation in maybe_custom_annotations
|
|
414
|
+
if isinstance(annotation, custom_annotation_type)
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def _get_argument_custom_help(cls, param: inspect.Parameter) -> Optional[str]:
|
|
419
|
+
"""Returns the help annotation for the given parameter."""
|
|
420
|
+
base_annotation = param.annotation
|
|
421
|
+
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
|
|
422
|
+
help_annotation = (
|
|
423
|
+
custom_annotations[0].description if custom_annotations else None
|
|
424
|
+
)
|
|
425
|
+
if not help_annotation:
|
|
426
|
+
# try to get it from the docstring
|
|
427
|
+
pass
|
|
428
|
+
return help_annotation
|
|
429
|
+
|
|
430
|
+
@classmethod
|
|
431
|
+
def _get_argument_custom_choices(cls, param: inspect.Parameter) -> Optional[str]:
|
|
432
|
+
"""Returns the help annotation for the given parameter."""
|
|
433
|
+
base_annotation = param.annotation
|
|
434
|
+
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
|
|
435
|
+
choices_annotation = (
|
|
436
|
+
custom_annotations[0].choices if custom_annotations else None
|
|
437
|
+
)
|
|
438
|
+
if not choices_annotation:
|
|
439
|
+
# try to get it from the docstring
|
|
440
|
+
pass
|
|
441
|
+
return choices_annotation
|
|
442
|
+
|
|
443
|
+
def _get_nargs(self, param: inspect.Parameter) -> Optional[str]:
|
|
444
|
+
"""Returns the nargs annotation for the given parameter."""
|
|
445
|
+
param_type = self.type_hints[param.name]
|
|
446
|
+
origin = get_origin(param_type)
|
|
447
|
+
|
|
448
|
+
if origin is list:
|
|
449
|
+
return "+"
|
|
450
|
+
|
|
451
|
+
if origin is Union and any(
|
|
452
|
+
get_origin(arg) is list for arg in get_args(param_type)
|
|
453
|
+
):
|
|
454
|
+
return "+"
|
|
455
|
+
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
def _generate_argparse_arguments(self, parameters) -> None:
|
|
459
|
+
"""Generates the argparse arguments from the function parameters."""
|
|
460
|
+
for param in parameters.values():
|
|
461
|
+
# TODO : how to handle kwargs?
|
|
462
|
+
# it's possible to add unknown arguments when parsing as follows:
|
|
463
|
+
# args, unknown_args = parser.parse_known_args()
|
|
464
|
+
if param.name == "kwargs":
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
param_type, choices = self._get_type_and_choices(param)
|
|
468
|
+
|
|
469
|
+
# if the param is a custom type, we need to flatten it
|
|
470
|
+
if inspect.isclass(param_type) and issubclass(param_type, BaseModel):
|
|
471
|
+
# update type hints with the custom type fields
|
|
472
|
+
type_hints = get_type_hints(param_type)
|
|
473
|
+
# prefix the type hints keys with the param name
|
|
474
|
+
type_hints = {
|
|
475
|
+
f"{param.name}{SEP}{key}": value
|
|
476
|
+
for key, value in type_hints.items()
|
|
477
|
+
}
|
|
478
|
+
self.type_hints.update(type_hints)
|
|
479
|
+
# create a signature from the custom type
|
|
480
|
+
sig = inspect.signature(param_type)
|
|
481
|
+
|
|
482
|
+
# add help to the annotation
|
|
483
|
+
annotated_parameters: List[inspect.Parameter] = []
|
|
484
|
+
for child_param in sig.parameters.values():
|
|
485
|
+
new_child_param = child_param.replace(
|
|
486
|
+
name=f"{param.name}{SEP}{child_param.name}",
|
|
487
|
+
annotation=Annotated[
|
|
488
|
+
child_param.annotation,
|
|
489
|
+
OpenBBField(
|
|
490
|
+
description=param_type.model_json_schema()[
|
|
491
|
+
"properties"
|
|
492
|
+
][child_param.name].get("description", None)
|
|
493
|
+
),
|
|
494
|
+
],
|
|
495
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
496
|
+
)
|
|
497
|
+
annotated_parameters.append(new_child_param)
|
|
498
|
+
|
|
499
|
+
# replacing with the annotated parameters
|
|
500
|
+
new_signature = inspect.Signature(
|
|
501
|
+
parameters=annotated_parameters,
|
|
502
|
+
return_annotation=sig.return_annotation,
|
|
503
|
+
)
|
|
504
|
+
self._generate_argparse_arguments(new_signature.parameters)
|
|
505
|
+
|
|
506
|
+
# the custom type itself should not be added as an argument
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
required = not self._param_is_default(param)
|
|
510
|
+
|
|
511
|
+
kwargs = {
|
|
512
|
+
"type": param_type,
|
|
513
|
+
"dest": param.name,
|
|
514
|
+
"default": param.default,
|
|
515
|
+
"required": required,
|
|
516
|
+
"action": self._get_action_type(param),
|
|
517
|
+
"help": self._get_argument_custom_help(param),
|
|
518
|
+
"nargs": self._get_nargs(param),
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if choices:
|
|
522
|
+
kwargs["choices"] = choices
|
|
523
|
+
|
|
524
|
+
if param_type == bool:
|
|
525
|
+
# store_true action does not accept the below kwargs
|
|
526
|
+
kwargs.pop("type")
|
|
527
|
+
kwargs.pop("nargs")
|
|
528
|
+
|
|
529
|
+
if required:
|
|
530
|
+
self._required.add_argument(
|
|
531
|
+
f"--{param.name}",
|
|
532
|
+
**kwargs,
|
|
533
|
+
)
|
|
534
|
+
else:
|
|
535
|
+
self._parser.add_argument(
|
|
536
|
+
f"--{param.name}",
|
|
537
|
+
**kwargs,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
@staticmethod
|
|
541
|
+
def _unflatten_args(args: dict) -> Dict[str, Any]:
|
|
542
|
+
"""Unflatten the args that were flattened by the custom types."""
|
|
543
|
+
result: Dict[str, Any] = {}
|
|
544
|
+
for key, value in args.items():
|
|
545
|
+
if SEP in key:
|
|
546
|
+
parts = key.split(SEP)
|
|
547
|
+
nested_dict = result
|
|
548
|
+
for part in parts[:-1]:
|
|
549
|
+
if part not in nested_dict:
|
|
550
|
+
nested_dict[part] = {}
|
|
551
|
+
nested_dict = nested_dict[part]
|
|
552
|
+
nested_dict[parts[-1]] = value
|
|
553
|
+
else:
|
|
554
|
+
result[key] = value
|
|
555
|
+
return result
|
|
556
|
+
|
|
557
|
+
def _update_with_custom_types(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
558
|
+
"""Update the kwargs with the custom types."""
|
|
559
|
+
# for each argument in the signature that is a custom type, we need to
|
|
560
|
+
# update the kwargs with the custom type kwargs
|
|
561
|
+
for param in self.signature.parameters.values():
|
|
562
|
+
# TODO : how to handle kwargs?
|
|
563
|
+
if param.name == "kwargs":
|
|
564
|
+
continue
|
|
565
|
+
param_type, _ = self._get_type_and_choices(param)
|
|
566
|
+
if inspect.isclass(param_type) and issubclass(param_type, BaseModel):
|
|
567
|
+
custom_type_kwargs = kwargs[param.name]
|
|
568
|
+
kwargs[param.name] = param_type(**custom_type_kwargs)
|
|
569
|
+
|
|
570
|
+
return kwargs
|
|
571
|
+
|
|
572
|
+
def execute_func(
|
|
573
|
+
self,
|
|
574
|
+
parsed_args: Optional[argparse.Namespace] = None,
|
|
575
|
+
) -> Any:
|
|
576
|
+
"""
|
|
577
|
+
Executes the original function with the parsed arguments.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
parsed_args (Optional[argparse.Namespace], optional): The parsed arguments. Defaults to None.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Any: The return value of the original function.
|
|
584
|
+
|
|
585
|
+
"""
|
|
586
|
+
kwargs = self._unflatten_args(vars(parsed_args))
|
|
587
|
+
kwargs = self._update_with_custom_types(kwargs)
|
|
588
|
+
|
|
589
|
+
provider = kwargs.get("provider")
|
|
590
|
+
provider_args = []
|
|
591
|
+
if provider and provider in self.provider_parameters:
|
|
592
|
+
provider_args = self.provider_parameters[provider]
|
|
593
|
+
else:
|
|
594
|
+
for args in self.provider_parameters.values():
|
|
595
|
+
provider_args.extend(args)
|
|
596
|
+
|
|
597
|
+
# remove kwargs that doesn't match the signature or provider parameters
|
|
598
|
+
kwargs = {
|
|
599
|
+
key: value
|
|
600
|
+
for key, value in kwargs.items()
|
|
601
|
+
if key in self.signature.parameters or key in provider_args
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return self.func(**kwargs)
|
|
605
|
+
|
|
606
|
+
def parse_args_and_execute(self) -> Any:
|
|
607
|
+
"""
|
|
608
|
+
Parses the arguments and executes the original function.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
Any: The return value of the original function.
|
|
612
|
+
"""
|
|
613
|
+
parsed_args = self._parser.parse_args()
|
|
614
|
+
return self.execute_func(parsed_args)
|
|
615
|
+
|
|
616
|
+
def translate(self) -> Callable:
|
|
617
|
+
"""
|
|
618
|
+
Wraps the original function with an argparse program.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Callable: The original function wrapped with an argparse program.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
def wrapper_func():
|
|
625
|
+
return self.parse_args_and_execute()
|
|
626
|
+
|
|
627
|
+
return wrapper_func
|