openbb-cli 1.0.0__tar.gz → 1.0.2__tar.gz
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-1.0.0 → openbb_cli-1.0.2}/PKG-INFO +7 -6
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/README.md +4 -4
- openbb_cli-1.0.2/openbb_cli/argparse_translator/argparse_argument.py +63 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/argparse_translator/argparse_class_processor.py +7 -8
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/argparse_translator/argparse_translator.py +70 -255
- openbb_cli-1.0.2/openbb_cli/argparse_translator/reference_processor.py +137 -0
- openbb_cli-1.0.2/openbb_cli/argparse_translator/utils.py +71 -0
- openbb_cli-1.0.2/openbb_cli/cli.py +23 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/base_controller.py +35 -12
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/base_platform_controller.py +1 -2
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/choices.py +7 -5
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/cli_controller.py +13 -9
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/utils.py +5 -2
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/models/settings.py +5 -2
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/pyproject.toml +4 -6
- openbb_cli-1.0.0/openbb_cli/cli.py +0 -20
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/__init__.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/argparse_translator/__init__.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/argparse_translator/obbject_registry.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/routines/routine_example.openbb +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/Consolas.ttf +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/dark.mpfstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/dark.mplrc.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/dark.mplstyle +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/dark.pltstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/dark.richstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/light.mpfstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/light.mplrc.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/light.mplstyle +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/light.pltstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/light.richstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/default/tables.pltstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/assets/styles/user/openbb.richstyle.json +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/config/__init__.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/config/completer.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/config/console.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/config/constants.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/config/menu_text.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/config/setup.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/config/style.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/hub_service.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/platform_controller_factory.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/script_parser.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/controllers/settings_controller.py +0 -0
- {openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/session.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: openbb-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Investment Research for Everyone, Anywhere.
|
|
5
5
|
Home-page: https://openbb.co
|
|
6
6
|
License: AGPL-3.0-only
|
|
@@ -12,7 +12,8 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Requires-Dist: openbb
|
|
15
|
+
Requires-Dist: openbb-core (>=1.2.4,<2.0.0)
|
|
16
|
+
Requires-Dist: openbb[all] (>=4.2.1,<5.0.0)
|
|
16
17
|
Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
|
|
17
18
|
Requires-Dist: prompt-toolkit (>=3.0.16,<4.0.0)
|
|
18
19
|
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
@@ -33,11 +34,11 @@ Description-Content-Type: text/markdown
|
|
|
33
34
|
|
|
34
35
|
## Overview
|
|
35
36
|
|
|
36
|
-
The OpenBB CLI is a command line interface that wraps [OpenBB Platform](https://docs.openbb.co/platform).
|
|
37
|
+
The OpenBB Platform CLI is a command line interface that wraps [OpenBB Platform](https://docs.openbb.co/platform).
|
|
37
38
|
|
|
38
39
|
It offers a convenient way to interact with the OpenBB Platform and its extensions, as well as automate data collection via OpenBB Routine Scripts.
|
|
39
40
|
|
|
40
|
-
Find the most complete documentation, examples, and usage guides for the OpenBB Platform CLI [here](https://
|
|
41
|
+
Find the most complete documentation, examples, and usage guides for the OpenBB Platform CLI [here](https://docs.openbb.co/cli).
|
|
41
42
|
|
|
42
43
|
## Installation
|
|
43
44
|
|
|
@@ -47,9 +48,9 @@ The command below provides access to the all the available OpenBB extensions beh
|
|
|
47
48
|
pip install openbb-cli
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
> Note: Find the most complete installation hints and tips [here](https://
|
|
51
|
+
> Note: Find the most complete installation hints and tips [here](https://docs.openbb.co/cli/installation).
|
|
51
52
|
|
|
52
|
-
After the installation is complete, you can deploy the OpenBB CLI by running the following command:
|
|
53
|
+
After the installation is complete, you can deploy the OpenBB Platform CLI by running the following command:
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
56
|
openbb
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
13
|
-
The OpenBB CLI is a command line interface that wraps [OpenBB Platform](https://docs.openbb.co/platform).
|
|
13
|
+
The OpenBB Platform CLI is a command line interface that wraps [OpenBB Platform](https://docs.openbb.co/platform).
|
|
14
14
|
|
|
15
15
|
It offers a convenient way to interact with the OpenBB Platform and its extensions, as well as automate data collection via OpenBB Routine Scripts.
|
|
16
16
|
|
|
17
|
-
Find the most complete documentation, examples, and usage guides for the OpenBB Platform CLI [here](https://
|
|
17
|
+
Find the most complete documentation, examples, and usage guides for the OpenBB Platform CLI [here](https://docs.openbb.co/cli).
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
@@ -24,9 +24,9 @@ The command below provides access to the all the available OpenBB extensions beh
|
|
|
24
24
|
pip install openbb-cli
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
> Note: Find the most complete installation hints and tips [here](https://
|
|
27
|
+
> Note: Find the most complete installation hints and tips [here](https://docs.openbb.co/cli/installation).
|
|
28
28
|
|
|
29
|
-
After the installation is complete, you can deploy the OpenBB CLI by running the following command:
|
|
29
|
+
After the installation is complete, you can deploy the OpenBB Platform CLI by running the following command:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
openbb
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Pydantic models for argparse arguments and argument groups."""
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
List,
|
|
6
|
+
Literal,
|
|
7
|
+
Optional,
|
|
8
|
+
Tuple,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, model_validator
|
|
12
|
+
|
|
13
|
+
SEP = "__"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ArgparseArgumentModel(BaseModel):
|
|
17
|
+
"""Pydantic model for an argparse argument."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
type: Any
|
|
21
|
+
dest: str
|
|
22
|
+
default: Any
|
|
23
|
+
required: bool
|
|
24
|
+
action: Literal["store_true", "store"]
|
|
25
|
+
help: Optional[str]
|
|
26
|
+
nargs: Optional[Literal["+"]]
|
|
27
|
+
choices: Optional[Tuple]
|
|
28
|
+
|
|
29
|
+
@model_validator(mode="after") # type: ignore
|
|
30
|
+
@classmethod
|
|
31
|
+
def validate_action(cls, values: "ArgparseArgumentModel"):
|
|
32
|
+
"""Validate the action based on the type."""
|
|
33
|
+
if values.type is bool and values.action != "store_true":
|
|
34
|
+
raise ValueError('If type is bool, action must be "store_true"')
|
|
35
|
+
return values
|
|
36
|
+
|
|
37
|
+
@model_validator(mode="after") # type: ignore
|
|
38
|
+
@classmethod
|
|
39
|
+
def remove_props_on_store_true(cls, values: "ArgparseArgumentModel"):
|
|
40
|
+
"""Remove type, nargs, and choices if action is store_true."""
|
|
41
|
+
if values.action == "store_true":
|
|
42
|
+
values.type = None
|
|
43
|
+
values.nargs = None
|
|
44
|
+
values.choices = None
|
|
45
|
+
return values
|
|
46
|
+
|
|
47
|
+
# override
|
|
48
|
+
def model_dump(self, **kwargs):
|
|
49
|
+
"""Override the model_dump method to remove empty choices."""
|
|
50
|
+
res = super().model_dump(**kwargs)
|
|
51
|
+
|
|
52
|
+
# Check if choices is present and if it's an empty tuple remove it
|
|
53
|
+
if "choices" in res and not res["choices"]:
|
|
54
|
+
del res["choices"]
|
|
55
|
+
|
|
56
|
+
return res
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ArgparseArgumentGroupModel(BaseModel):
|
|
60
|
+
"""Pydantic model for a custom argument group."""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
arguments: List[ArgparseArgumentModel]
|
{openbb_cli-1.0.0 → openbb_cli-1.0.2}/openbb_cli/argparse_translator/argparse_class_processor.py
RENAMED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
+
"""Module for the ArgparseClassProcessor class."""
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
from typing import Any, Dict, Optional, Type
|
|
3
5
|
|
|
4
6
|
# TODO: this needs to be done differently
|
|
5
7
|
from openbb_core.app.static.container import Container
|
|
6
8
|
|
|
7
|
-
from openbb_cli.argparse_translator.argparse_translator import
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
from openbb_cli.argparse_translator.argparse_translator import ArgparseTranslator
|
|
10
|
+
from openbb_cli.argparse_translator.reference_processor import (
|
|
11
|
+
ReferenceToArgumentsProcessor,
|
|
10
12
|
)
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class ArgparseClassProcessor:
|
|
14
|
-
"""
|
|
15
|
-
Process a target class to create ArgparseTranslators for its methods.
|
|
16
|
-
"""
|
|
16
|
+
"""Process a target class to create ArgparseTranslators for its methods."""
|
|
17
17
|
|
|
18
18
|
# reference variable used to create custom groups for the ArgpaseTranslators
|
|
19
19
|
_reference: Dict[str, Any] = {}
|
|
@@ -77,7 +77,7 @@ class ArgparseClassProcessor:
|
|
|
77
77
|
reference = {route: cls._reference[route]} if route in cls._reference else {}
|
|
78
78
|
if not reference:
|
|
79
79
|
return {}
|
|
80
|
-
rp =
|
|
80
|
+
rp = ReferenceToArgumentsProcessor(reference)
|
|
81
81
|
return rp.custom_groups.get(route, {}) # type: ignore
|
|
82
82
|
|
|
83
83
|
@classmethod
|
|
@@ -92,7 +92,6 @@ class ArgparseClassProcessor:
|
|
|
92
92
|
if name.startswith("__") or name.startswith("_"):
|
|
93
93
|
continue
|
|
94
94
|
if inspect.ismethod(member):
|
|
95
|
-
|
|
96
95
|
class_name = cls._get_class_name(target)
|
|
97
96
|
methods[f"{class_name}_{name}"] = ArgparseTranslator(
|
|
98
97
|
func=member,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
"""Module for translating a function into an argparse program."""
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
2
4
|
import inspect
|
|
3
5
|
import re
|
|
4
6
|
from copy import deepcopy
|
|
5
|
-
from enum import Enum
|
|
6
7
|
from typing import (
|
|
7
8
|
Any,
|
|
8
9
|
Callable,
|
|
@@ -19,184 +20,37 @@ from typing import (
|
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
from openbb_core.app.model.field import OpenBBField
|
|
22
|
-
from pydantic import BaseModel
|
|
23
|
+
from pydantic import BaseModel
|
|
23
24
|
from typing_extensions import Annotated
|
|
24
25
|
|
|
26
|
+
from openbb_cli.argparse_translator.argparse_argument import (
|
|
27
|
+
ArgparseArgumentGroupModel,
|
|
28
|
+
ArgparseArgumentModel,
|
|
29
|
+
)
|
|
30
|
+
from openbb_cli.argparse_translator.utils import (
|
|
31
|
+
get_argument_choices,
|
|
32
|
+
get_argument_optional_choices,
|
|
33
|
+
in_group,
|
|
34
|
+
remove_argument,
|
|
35
|
+
set_optional_choices,
|
|
36
|
+
)
|
|
37
|
+
|
|
25
38
|
# pylint: disable=protected-access
|
|
26
39
|
|
|
27
40
|
SEP = "__"
|
|
28
41
|
|
|
29
42
|
|
|
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
43
|
class ArgparseTranslator:
|
|
44
|
+
"""Class to translate a function into an argparse program."""
|
|
45
|
+
|
|
192
46
|
def __init__(
|
|
193
47
|
self,
|
|
194
48
|
func: Callable,
|
|
195
|
-
custom_argument_groups: Optional[List[
|
|
49
|
+
custom_argument_groups: Optional[List[ArgparseArgumentGroupModel]] = None,
|
|
196
50
|
add_help: Optional[bool] = True,
|
|
197
51
|
):
|
|
198
52
|
"""
|
|
199
|
-
|
|
53
|
+
Initialize the ArgparseTranslator.
|
|
200
54
|
|
|
201
55
|
Args:
|
|
202
56
|
func (Callable): The function to translate into an argparse program.
|
|
@@ -228,45 +82,6 @@ class ArgparseTranslator:
|
|
|
228
82
|
def _handle_argument_in_groups(self, argument, group):
|
|
229
83
|
"""Handle the argument and add it to the parser."""
|
|
230
84
|
|
|
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
85
|
def _update_providers(
|
|
271
86
|
input_string: str, new_provider: List[Optional[str]]
|
|
272
87
|
) -> str:
|
|
@@ -288,23 +103,27 @@ class ArgparseTranslator:
|
|
|
288
103
|
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
|
|
289
104
|
model_choices = kwargs.get("choices", ()) or ()
|
|
290
105
|
# extend choices
|
|
291
|
-
|
|
106
|
+
existing_choices = get_argument_choices(self._parser, argument.name)
|
|
107
|
+
choices = tuple(set(existing_choices + model_choices))
|
|
108
|
+
optional_choices = bool(existing_choices and not model_choices)
|
|
292
109
|
|
|
293
110
|
# check if the argument is in the required arguments
|
|
294
|
-
if
|
|
111
|
+
if in_group(self._parser, argument.name, group_title="required arguments"):
|
|
295
112
|
for action in self._required._group_actions:
|
|
296
113
|
if action.dest == argument.name and choices:
|
|
297
114
|
# update choices
|
|
298
115
|
action.choices = choices
|
|
116
|
+
set_optional_choices(action, optional_choices)
|
|
299
117
|
return
|
|
300
118
|
|
|
301
119
|
# check if the argument is in the optional arguments
|
|
302
|
-
if
|
|
120
|
+
if in_group(self._parser, argument.name, group_title="optional arguments"):
|
|
303
121
|
for action in self._parser._actions:
|
|
304
122
|
if action.dest == argument.name:
|
|
305
123
|
# update choices
|
|
306
124
|
if choices:
|
|
307
125
|
action.choices = choices
|
|
126
|
+
set_optional_choices(action, optional_choices)
|
|
308
127
|
if argument.name not in self.signature.parameters:
|
|
309
128
|
# update help
|
|
310
129
|
action.help = _update_providers(
|
|
@@ -312,9 +131,16 @@ class ArgparseTranslator:
|
|
|
312
131
|
)
|
|
313
132
|
return
|
|
314
133
|
|
|
134
|
+
# we need to check if the optional choices were set in other group
|
|
135
|
+
# before we remove the argument from the group, otherwise we will lose info
|
|
136
|
+
if not optional_choices:
|
|
137
|
+
optional_choices = get_argument_optional_choices(
|
|
138
|
+
self._parser, argument.name
|
|
139
|
+
)
|
|
140
|
+
|
|
315
141
|
# if the argument is in use, remove it from all groups
|
|
316
142
|
# and return the groups that had the argument
|
|
317
|
-
groups_w_arg =
|
|
143
|
+
groups_w_arg = remove_argument(self._parser, argument.name)
|
|
318
144
|
groups_w_arg.append(group.title) # add current group
|
|
319
145
|
|
|
320
146
|
# add it to the optional arguments group instead
|
|
@@ -322,16 +148,17 @@ class ArgparseTranslator:
|
|
|
322
148
|
kwargs["choices"] = choices # update choices
|
|
323
149
|
# add provider info to the help
|
|
324
150
|
kwargs["help"] = _update_providers(argument.help or "", groups_w_arg)
|
|
325
|
-
self._parser.add_argument(f"--{argument.name}", **kwargs)
|
|
151
|
+
action = self._parser.add_argument(f"--{argument.name}", **kwargs)
|
|
152
|
+
set_optional_choices(action, optional_choices)
|
|
326
153
|
|
|
327
154
|
@property
|
|
328
155
|
def parser(self) -> argparse.ArgumentParser:
|
|
156
|
+
"""Get the argparse parser."""
|
|
329
157
|
return deepcopy(self._parser)
|
|
330
158
|
|
|
331
159
|
@staticmethod
|
|
332
160
|
def _build_description(func_doc: str) -> str:
|
|
333
|
-
"""
|
|
334
|
-
|
|
161
|
+
"""Build the description of the argparse program from the function docstring."""
|
|
335
162
|
patterns = ["openbb\n ======", "Parameters\n ----------"]
|
|
336
163
|
|
|
337
164
|
if func_doc:
|
|
@@ -344,31 +171,34 @@ class ArgparseTranslator:
|
|
|
344
171
|
|
|
345
172
|
@staticmethod
|
|
346
173
|
def _param_is_default(param: inspect.Parameter) -> bool:
|
|
347
|
-
"""
|
|
174
|
+
"""Return True if the parameter has a default value."""
|
|
348
175
|
return param.default != inspect.Parameter.empty
|
|
349
176
|
|
|
350
177
|
def _get_action_type(self, param: inspect.Parameter) -> str:
|
|
351
|
-
"""
|
|
178
|
+
"""Return the argparse action type for the given parameter."""
|
|
352
179
|
param_type = self.type_hints[param.name]
|
|
180
|
+
type_origin = get_origin(param_type)
|
|
353
181
|
|
|
354
|
-
if param_type == bool
|
|
355
|
-
|
|
356
|
-
|
|
182
|
+
if param_type == bool or (
|
|
183
|
+
type_origin is Union and bool in get_args(param_type)
|
|
184
|
+
):
|
|
185
|
+
return "store_true"
|
|
186
|
+
return "store"
|
|
357
187
|
|
|
358
188
|
def _get_type_and_choices(
|
|
359
189
|
self, param: inspect.Parameter
|
|
360
190
|
) -> Tuple[Type[Any], Tuple[Any, ...]]:
|
|
361
|
-
"""
|
|
191
|
+
"""Return the type and choices for the given parameter."""
|
|
362
192
|
param_type = self.type_hints[param.name]
|
|
363
193
|
type_origin = get_origin(param_type)
|
|
364
194
|
|
|
365
|
-
choices = ()
|
|
195
|
+
choices: tuple[Any, ...] = ()
|
|
366
196
|
|
|
367
197
|
if type_origin is Literal:
|
|
368
198
|
choices = get_args(param_type)
|
|
369
199
|
param_type = type(choices[0]) # type: ignore
|
|
370
200
|
|
|
371
|
-
if type_origin is list:
|
|
201
|
+
if type_origin is list:
|
|
372
202
|
param_type = get_args(param_type)[0]
|
|
373
203
|
|
|
374
204
|
if get_origin(param_type) is Literal:
|
|
@@ -416,32 +246,26 @@ class ArgparseTranslator:
|
|
|
416
246
|
|
|
417
247
|
@classmethod
|
|
418
248
|
def _get_argument_custom_help(cls, param: inspect.Parameter) -> Optional[str]:
|
|
419
|
-
"""
|
|
249
|
+
"""Return the help annotation for the given parameter."""
|
|
420
250
|
base_annotation = param.annotation
|
|
421
251
|
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
|
|
422
252
|
help_annotation = (
|
|
423
253
|
custom_annotations[0].description if custom_annotations else None
|
|
424
254
|
)
|
|
425
|
-
if not help_annotation:
|
|
426
|
-
# try to get it from the docstring
|
|
427
|
-
pass
|
|
428
255
|
return help_annotation
|
|
429
256
|
|
|
430
257
|
@classmethod
|
|
431
258
|
def _get_argument_custom_choices(cls, param: inspect.Parameter) -> Optional[str]:
|
|
432
|
-
"""
|
|
259
|
+
"""Return the help annotation for the given parameter."""
|
|
433
260
|
base_annotation = param.annotation
|
|
434
261
|
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
|
|
435
262
|
choices_annotation = (
|
|
436
263
|
custom_annotations[0].choices if custom_annotations else None
|
|
437
264
|
)
|
|
438
|
-
if not choices_annotation:
|
|
439
|
-
# try to get it from the docstring
|
|
440
|
-
pass
|
|
441
265
|
return choices_annotation
|
|
442
266
|
|
|
443
267
|
def _get_nargs(self, param: inspect.Parameter) -> Optional[str]:
|
|
444
|
-
"""
|
|
268
|
+
"""Return the nargs annotation for the given parameter."""
|
|
445
269
|
param_type = self.type_hints[param.name]
|
|
446
270
|
origin = get_origin(param_type)
|
|
447
271
|
|
|
@@ -456,11 +280,8 @@ class ArgparseTranslator:
|
|
|
456
280
|
return None
|
|
457
281
|
|
|
458
282
|
def _generate_argparse_arguments(self, parameters) -> None:
|
|
459
|
-
"""
|
|
283
|
+
"""Generate the argparse arguments from the function parameters."""
|
|
460
284
|
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
285
|
if param.name == "kwargs":
|
|
465
286
|
continue
|
|
466
287
|
|
|
@@ -508,32 +329,27 @@ class ArgparseTranslator:
|
|
|
508
329
|
|
|
509
330
|
required = not self._param_is_default(param)
|
|
510
331
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if param_type == bool:
|
|
525
|
-
# store_true action does not accept the below kwargs
|
|
526
|
-
kwargs.pop("type")
|
|
527
|
-
kwargs.pop("nargs")
|
|
332
|
+
argument = ArgparseArgumentModel(
|
|
333
|
+
name=param.name,
|
|
334
|
+
type=param_type,
|
|
335
|
+
dest=param.name,
|
|
336
|
+
default=param.default,
|
|
337
|
+
required=required,
|
|
338
|
+
action=self._get_action_type(param),
|
|
339
|
+
help=self._get_argument_custom_help(param),
|
|
340
|
+
nargs=self._get_nargs(param),
|
|
341
|
+
choices=choices,
|
|
342
|
+
)
|
|
343
|
+
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
|
|
528
344
|
|
|
529
345
|
if required:
|
|
530
346
|
self._required.add_argument(
|
|
531
|
-
f"--{
|
|
347
|
+
f"--{argument.name}",
|
|
532
348
|
**kwargs,
|
|
533
349
|
)
|
|
534
350
|
else:
|
|
535
351
|
self._parser.add_argument(
|
|
536
|
-
f"--{
|
|
352
|
+
f"--{argument.name}",
|
|
537
353
|
**kwargs,
|
|
538
354
|
)
|
|
539
355
|
|
|
@@ -559,7 +375,6 @@ class ArgparseTranslator:
|
|
|
559
375
|
# for each argument in the signature that is a custom type, we need to
|
|
560
376
|
# update the kwargs with the custom type kwargs
|
|
561
377
|
for param in self.signature.parameters.values():
|
|
562
|
-
# TODO : how to handle kwargs?
|
|
563
378
|
if param.name == "kwargs":
|
|
564
379
|
continue
|
|
565
380
|
param_type, _ = self._get_type_and_choices(param)
|
|
@@ -574,7 +389,7 @@ class ArgparseTranslator:
|
|
|
574
389
|
parsed_args: Optional[argparse.Namespace] = None,
|
|
575
390
|
) -> Any:
|
|
576
391
|
"""
|
|
577
|
-
|
|
392
|
+
Execute the original function with the parsed arguments.
|
|
578
393
|
|
|
579
394
|
Args:
|
|
580
395
|
parsed_args (Optional[argparse.Namespace], optional): The parsed arguments. Defaults to None.
|
|
@@ -605,7 +420,7 @@ class ArgparseTranslator:
|
|
|
605
420
|
|
|
606
421
|
def parse_args_and_execute(self) -> Any:
|
|
607
422
|
"""
|
|
608
|
-
|
|
423
|
+
Parse the arguments and executes the original function.
|
|
609
424
|
|
|
610
425
|
Returns:
|
|
611
426
|
Any: The return value of the original function.
|
|
@@ -615,7 +430,7 @@ class ArgparseTranslator:
|
|
|
615
430
|
|
|
616
431
|
def translate(self) -> Callable:
|
|
617
432
|
"""
|
|
618
|
-
|
|
433
|
+
Wrap the original function with an argparse program.
|
|
619
434
|
|
|
620
435
|
Returns:
|
|
621
436
|
Callable: The original function wrapped with an argparse program.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Module for the ReferenceToArgumentsProcessor class."""
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Dict,
|
|
6
|
+
List,
|
|
7
|
+
Literal,
|
|
8
|
+
Optional,
|
|
9
|
+
Tuple,
|
|
10
|
+
Union,
|
|
11
|
+
get_args,
|
|
12
|
+
get_origin,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from openbb_cli.argparse_translator.argparse_argument import (
|
|
16
|
+
ArgparseArgumentGroupModel,
|
|
17
|
+
ArgparseArgumentModel,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReferenceToArgumentsProcessor:
|
|
22
|
+
"""Class to process the reference and build custom argument groups."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, reference: Dict[str, Dict]):
|
|
25
|
+
"""Initialize the ReferenceToArgumentsProcessor."""
|
|
26
|
+
self._reference = reference
|
|
27
|
+
self._custom_groups: Dict[str, List[ArgparseArgumentGroupModel]] = {}
|
|
28
|
+
|
|
29
|
+
self._build_custom_groups()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def custom_groups(self) -> Dict[str, List[ArgparseArgumentGroupModel]]:
|
|
33
|
+
"""Get the custom groups."""
|
|
34
|
+
return self._custom_groups
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _make_type_parsable(type_: str) -> type:
|
|
38
|
+
"""Make the type parsable by removing the annotations."""
|
|
39
|
+
if "Union" in type_ and "str" in type_:
|
|
40
|
+
return str
|
|
41
|
+
if "Union" in type_ and "int" in type_:
|
|
42
|
+
return int
|
|
43
|
+
if type_ in ["date", "datetime.time", "time"]:
|
|
44
|
+
return str
|
|
45
|
+
|
|
46
|
+
if any(x in type_ for x in ["gt=", "ge=", "lt=", "le="]):
|
|
47
|
+
if "Annotated" in type_:
|
|
48
|
+
type_ = type_.replace("Annotated[", "").replace("]", "")
|
|
49
|
+
type_ = type_.split(",")[0]
|
|
50
|
+
|
|
51
|
+
return eval(type_) # noqa: S307, E501 pylint: disable=eval-used
|
|
52
|
+
|
|
53
|
+
def _parse_type(self, type_: str) -> type:
|
|
54
|
+
"""Parse the type from the string representation."""
|
|
55
|
+
type_ = self._make_type_parsable(type_) # type: ignore
|
|
56
|
+
|
|
57
|
+
if get_origin(type_) is Literal:
|
|
58
|
+
type_ = type(get_args(type_)[0]) # type: ignore
|
|
59
|
+
|
|
60
|
+
return type_ # type: ignore
|
|
61
|
+
|
|
62
|
+
def _get_nargs(self, type_: type) -> Optional[Union[int, str]]:
|
|
63
|
+
"""Get the nargs for the given type."""
|
|
64
|
+
if get_origin(type_) is list:
|
|
65
|
+
return "+"
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def _get_choices(self, type_: str, custom_choices: Any) -> Tuple:
|
|
69
|
+
"""Get the choices for the given type."""
|
|
70
|
+
type_ = self._make_type_parsable(type_) # type: ignore
|
|
71
|
+
type_origin = get_origin(type_)
|
|
72
|
+
|
|
73
|
+
choices: tuple[Any, ...] = ()
|
|
74
|
+
|
|
75
|
+
if type_origin is Literal:
|
|
76
|
+
choices = get_args(type_)
|
|
77
|
+
|
|
78
|
+
if type_origin is list:
|
|
79
|
+
type_ = get_args(type_)[0]
|
|
80
|
+
|
|
81
|
+
if get_origin(type_) is Literal:
|
|
82
|
+
choices = get_args(type_)
|
|
83
|
+
|
|
84
|
+
if type_origin is Union and type(None) in get_args(type_):
|
|
85
|
+
# remove NoneType from the args
|
|
86
|
+
args = [arg for arg in get_args(type_) if arg != type(None)]
|
|
87
|
+
# if there is only one arg left, use it
|
|
88
|
+
if len(args) > 1:
|
|
89
|
+
raise ValueError("Union with NoneType should have only one type left")
|
|
90
|
+
type_ = args[0]
|
|
91
|
+
|
|
92
|
+
if get_origin(type_) is Literal:
|
|
93
|
+
choices = get_args(type_)
|
|
94
|
+
|
|
95
|
+
if custom_choices:
|
|
96
|
+
return tuple(custom_choices)
|
|
97
|
+
|
|
98
|
+
return choices
|
|
99
|
+
|
|
100
|
+
def _build_custom_groups(self):
|
|
101
|
+
"""Build the custom groups from the reference."""
|
|
102
|
+
for route, v in self._reference.items():
|
|
103
|
+
for provider, args in v["parameters"].items():
|
|
104
|
+
if provider == "standard":
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
custom_arguments = []
|
|
108
|
+
for arg in args:
|
|
109
|
+
if arg.get("standard"):
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
type_ = self._parse_type(arg["type"])
|
|
113
|
+
|
|
114
|
+
custom_arguments.append(
|
|
115
|
+
ArgparseArgumentModel(
|
|
116
|
+
name=arg["name"],
|
|
117
|
+
type=type_,
|
|
118
|
+
dest=arg["name"],
|
|
119
|
+
default=arg["default"],
|
|
120
|
+
required=not (arg["optional"]),
|
|
121
|
+
action="store" if type_ != bool else "store_true",
|
|
122
|
+
help=arg["description"],
|
|
123
|
+
nargs=self._get_nargs(type_), # type: ignore
|
|
124
|
+
choices=self._get_choices(
|
|
125
|
+
arg["type"], custom_choices=arg["choices"]
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
group = ArgparseArgumentGroupModel(
|
|
131
|
+
name=provider, arguments=custom_arguments
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if route not in self._custom_groups:
|
|
135
|
+
self._custom_groups[route] = []
|
|
136
|
+
|
|
137
|
+
self._custom_groups[route].append(group)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Utilities for argparse_translator module."""
|
|
2
|
+
|
|
3
|
+
from argparse import Action, ArgumentParser
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def in_group(parser: ArgumentParser, argument_name: str, group_title: str) -> bool:
|
|
8
|
+
"""Check if an argument is in a group of an ArgumentParser."""
|
|
9
|
+
for action_group in parser._action_groups: # pylint: disable=protected-access
|
|
10
|
+
if action_group.title == group_title:
|
|
11
|
+
for (
|
|
12
|
+
action
|
|
13
|
+
) in action_group._group_actions: # pylint: disable=protected-access
|
|
14
|
+
opts = action.option_strings
|
|
15
|
+
if (opts and opts[0] == argument_name) or action.dest == argument_name:
|
|
16
|
+
return True
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def remove_argument(parser: ArgumentParser, argument_name: str) -> List[Optional[str]]:
|
|
21
|
+
"""Remove an argument from an ArgumentParser."""
|
|
22
|
+
groups_w_arg = []
|
|
23
|
+
|
|
24
|
+
# remove the argument from the parser
|
|
25
|
+
for action in parser._actions: # pylint: disable=protected-access
|
|
26
|
+
opts = action.option_strings
|
|
27
|
+
if (opts and opts[0] == argument_name) or action.dest == argument_name:
|
|
28
|
+
parser._remove_action(action) # pylint: disable=protected-access
|
|
29
|
+
break
|
|
30
|
+
|
|
31
|
+
# remove from all groups
|
|
32
|
+
for action_group in parser._action_groups: # pylint: disable=protected-access
|
|
33
|
+
for action in action_group._group_actions: # pylint: disable=protected-access
|
|
34
|
+
opts = action.option_strings
|
|
35
|
+
if (opts and opts[0] == argument_name) or action.dest == argument_name:
|
|
36
|
+
action_group._group_actions.remove( # pylint: disable=protected-access
|
|
37
|
+
action
|
|
38
|
+
)
|
|
39
|
+
groups_w_arg.append(action_group.title)
|
|
40
|
+
|
|
41
|
+
# remove from _action_groups dict
|
|
42
|
+
parser._option_string_actions.pop( # pylint: disable=protected-access
|
|
43
|
+
f"--{argument_name}", None
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return groups_w_arg
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_argument_choices(parser: ArgumentParser, argument_name: str) -> Tuple:
|
|
50
|
+
"""Get the choices of an argument from an ArgumentParser."""
|
|
51
|
+
for action in parser._actions: # pylint: disable=protected-access
|
|
52
|
+
opts = action.option_strings
|
|
53
|
+
if (opts and opts[0] == argument_name) or action.dest == argument_name:
|
|
54
|
+
return tuple(action.choices or ())
|
|
55
|
+
return ()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_argument_optional_choices(parser: ArgumentParser, argument_name: str) -> bool:
|
|
59
|
+
"""Get the optional_choices attribute of an argument from an ArgumentParser."""
|
|
60
|
+
for action in parser._actions: # pylint: disable=protected-access
|
|
61
|
+
opts = action.option_strings
|
|
62
|
+
if (opts and opts[0] == argument_name) or action.dest == argument_name:
|
|
63
|
+
if hasattr(action, "optional_choices"):
|
|
64
|
+
return action.optional_choices
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def set_optional_choices(action: Action, optional_choices: bool):
|
|
69
|
+
"""Set the optional_choices attribute of an action."""
|
|
70
|
+
if not hasattr(action, "optional_choices") and optional_choices:
|
|
71
|
+
setattr(action, "optional_choices", optional_choices)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""OpenBB Platform CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
"""Use the main entry point for the OpenBB Platform CLI."""
|
|
8
|
+
print("Loading...\n") # noqa: T201
|
|
9
|
+
|
|
10
|
+
# pylint: disable=import-outside-toplevel
|
|
11
|
+
from openbb_cli.config.setup import bootstrap
|
|
12
|
+
from openbb_cli.controllers.cli_controller import launch
|
|
13
|
+
|
|
14
|
+
bootstrap()
|
|
15
|
+
|
|
16
|
+
dev = "--dev" in sys.argv[1:]
|
|
17
|
+
debug = "--debug" in sys.argv[1:]
|
|
18
|
+
|
|
19
|
+
launch(dev, debug)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
import difflib
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import shlex
|
|
7
8
|
from abc import ABCMeta, abstractmethod
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
from pathlib import Path
|
|
@@ -240,7 +241,7 @@ class BaseController(metaclass=ABCMeta):
|
|
|
240
241
|
else:
|
|
241
242
|
try:
|
|
242
243
|
(known_args, other_args) = self.parser.parse_known_args(
|
|
243
|
-
|
|
244
|
+
shlex.split(an_input)
|
|
244
245
|
)
|
|
245
246
|
except Exception as exc:
|
|
246
247
|
raise SystemExit from exc
|
|
@@ -742,13 +743,15 @@ class BaseController(metaclass=ABCMeta):
|
|
|
742
743
|
|
|
743
744
|
if export_allowed == "raw_data_only":
|
|
744
745
|
choices_export = ["csv", "json", "xlsx"]
|
|
745
|
-
help_export = "Export raw data into csv, json
|
|
746
|
+
help_export = "Export raw data into csv, json or xlsx."
|
|
746
747
|
elif export_allowed == "figures_only":
|
|
747
|
-
choices_export = ["png", "jpg"
|
|
748
|
-
help_export = "Export figure into png
|
|
748
|
+
choices_export = ["png", "jpg"]
|
|
749
|
+
help_export = "Export figure into png or jpg."
|
|
749
750
|
else:
|
|
750
|
-
choices_export = ["csv", "json", "xlsx", "png", "jpg"
|
|
751
|
-
help_export =
|
|
751
|
+
choices_export = ["csv", "json", "xlsx", "png", "jpg"]
|
|
752
|
+
help_export = (
|
|
753
|
+
"Export raw data into csv, json, xlsx and figure into png or jpg."
|
|
754
|
+
)
|
|
752
755
|
|
|
753
756
|
parser.add_argument(
|
|
754
757
|
"--export",
|
|
@@ -798,11 +801,31 @@ class BaseController(metaclass=ABCMeta):
|
|
|
798
801
|
return None
|
|
799
802
|
|
|
800
803
|
try:
|
|
801
|
-
#
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
804
|
+
# Determine the index of the routine arguments
|
|
805
|
+
routine_args_index = next(
|
|
806
|
+
(
|
|
807
|
+
i + 1
|
|
808
|
+
for i, arg in enumerate(other_args)
|
|
809
|
+
if arg in ("-i", "--input")
|
|
810
|
+
and "routine_args"
|
|
811
|
+
in [
|
|
812
|
+
action.dest
|
|
813
|
+
for action in parser._actions # pylint: disable=protected-access
|
|
814
|
+
]
|
|
815
|
+
),
|
|
816
|
+
-1,
|
|
817
|
+
)
|
|
818
|
+
# Split comma-separated arguments, except for the argument at routine_args_index
|
|
819
|
+
other_args = [
|
|
820
|
+
part
|
|
821
|
+
for index, arg in enumerate(other_args)
|
|
822
|
+
for part in (arg.split(",") if index != routine_args_index else [arg])
|
|
823
|
+
]
|
|
824
|
+
|
|
825
|
+
# Check if the action has optional choices, if yes, remove them
|
|
826
|
+
for action in parser._actions: # pylint: disable=protected-access
|
|
827
|
+
if hasattr(action, "optional_choices") and action.optional_choices:
|
|
828
|
+
action.choices = None
|
|
806
829
|
|
|
807
830
|
(ns_parser, l_unknown_args) = parser.parse_known_args(other_args)
|
|
808
831
|
|
|
@@ -811,7 +834,7 @@ class BaseController(metaclass=ABCMeta):
|
|
|
811
834
|
"raw_data_and_figures",
|
|
812
835
|
]:
|
|
813
836
|
ns_parser.is_image = any(
|
|
814
|
-
ext in ns_parser.export for ext in ["png", "
|
|
837
|
+
ext in ns_parser.export for ext in ["png", "jpg"]
|
|
815
838
|
)
|
|
816
839
|
|
|
817
840
|
except SystemExit:
|
|
@@ -154,11 +154,10 @@ class PlatformController(BaseController):
|
|
|
154
154
|
|
|
155
155
|
obbject = translator.execute_func(parsed_args=ns_parser)
|
|
156
156
|
df: pd.DataFrame = pd.DataFrame()
|
|
157
|
-
fig: OpenBBFigure = None
|
|
157
|
+
fig: Optional[OpenBBFigure] = None
|
|
158
158
|
title = f"{self.PATH}{translator.func.__name__}"
|
|
159
159
|
|
|
160
160
|
if obbject:
|
|
161
|
-
|
|
162
161
|
if isinstance(obbject, OBBject):
|
|
163
162
|
if session.max_obbjects_exceeded() and obbject.results:
|
|
164
163
|
session.obbject_registry.remove()
|
|
@@ -56,13 +56,15 @@ def __mock_parse_known_args_and_warn(
|
|
|
56
56
|
|
|
57
57
|
if export_allowed == "raw_data_only":
|
|
58
58
|
choices_export = ["csv", "json", "xlsx"]
|
|
59
|
-
help_export = "Export raw data into csv, json
|
|
59
|
+
help_export = "Export raw data into csv, json or xlsx."
|
|
60
60
|
elif export_allowed == "figures_only":
|
|
61
|
-
choices_export = ["png", "jpg"
|
|
62
|
-
help_export = "Export figure into png
|
|
61
|
+
choices_export = ["png", "jpg"]
|
|
62
|
+
help_export = "Export figure into png or jpg."
|
|
63
63
|
else:
|
|
64
|
-
choices_export = ["csv", "json", "xlsx", "png", "jpg"
|
|
65
|
-
help_export =
|
|
64
|
+
choices_export = ["csv", "json", "xlsx", "png", "jpg"]
|
|
65
|
+
help_export = (
|
|
66
|
+
"Export raw data into csv, json, xlsx and figure into png or jpg."
|
|
67
|
+
)
|
|
66
68
|
|
|
67
69
|
parser.add_argument(
|
|
68
70
|
"--export",
|
|
@@ -355,7 +355,7 @@ class CLIController(BaseController):
|
|
|
355
355
|
"--input",
|
|
356
356
|
help="Select multiple inputs to be replaced in the routine and separated by commas. E.g. GME,AMC,BTC-USD",
|
|
357
357
|
dest="routine_args",
|
|
358
|
-
type=
|
|
358
|
+
type=str,
|
|
359
359
|
)
|
|
360
360
|
parser.add_argument(
|
|
361
361
|
"-e",
|
|
@@ -431,17 +431,22 @@ class CLIController(BaseController):
|
|
|
431
431
|
with open(routine_path) as fp:
|
|
432
432
|
raw_lines = list(fp)
|
|
433
433
|
|
|
434
|
+
script_inputs = []
|
|
434
435
|
# Capture ARGV either as list if args separated by commas or as single value
|
|
435
|
-
if ns_parser.routine_args:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
436
|
+
if routine_args := ns_parser.routine_args:
|
|
437
|
+
pattern = r"\[(.*?)\]"
|
|
438
|
+
matches = re.findall(pattern, routine_args)
|
|
439
|
+
|
|
440
|
+
for match in matches:
|
|
441
|
+
routine_args = routine_args.replace(f"[{match}]", "")
|
|
442
|
+
script_inputs.append(match)
|
|
443
|
+
|
|
444
|
+
script_inputs.extend(
|
|
445
|
+
[val for val in routine_args.split(",") if val]
|
|
440
446
|
)
|
|
441
447
|
|
|
442
448
|
err, parsed_script = parse_openbb_script(
|
|
443
|
-
raw_lines=raw_lines,
|
|
444
|
-
script_inputs=script_inputs if ns_parser.routine_args else None,
|
|
449
|
+
raw_lines=raw_lines, script_inputs=script_inputs
|
|
445
450
|
)
|
|
446
451
|
|
|
447
452
|
# If there err output is not an empty string then it means there was an
|
|
@@ -546,7 +551,6 @@ def run_cli(jobs_cmds: Optional[List[str]] = None, test_mode=False):
|
|
|
546
551
|
t_controller.print_help()
|
|
547
552
|
|
|
548
553
|
while ret_code:
|
|
549
|
-
|
|
550
554
|
# There is a command in the queue
|
|
551
555
|
if t_controller.queue and len(t_controller.queue) > 0:
|
|
552
556
|
# If the command is quitting the menu we want to return in here
|
|
@@ -901,7 +901,7 @@ def export_data(
|
|
|
901
901
|
else:
|
|
902
902
|
save_to_excel(df, saved_path, sheet_name)
|
|
903
903
|
|
|
904
|
-
elif saved_path.suffix in [".jpg", ".
|
|
904
|
+
elif saved_path.suffix in [".jpg", ".png"]:
|
|
905
905
|
if figure is None:
|
|
906
906
|
Session().console.print("No plot to export.")
|
|
907
907
|
continue
|
|
@@ -910,7 +910,10 @@ def export_data(
|
|
|
910
910
|
Session().console.print("Wrong export file specified.")
|
|
911
911
|
continue
|
|
912
912
|
|
|
913
|
-
|
|
913
|
+
if saved_path.exists():
|
|
914
|
+
Session().console.print(f"Saved file: {saved_path}")
|
|
915
|
+
else:
|
|
916
|
+
Session().console.print(f"Failed to save file: {saved_path}")
|
|
914
917
|
|
|
915
918
|
if figure is not None:
|
|
916
919
|
figure._exported = True # pylint: disable=protected-access
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
"""Settings model"""
|
|
1
|
+
"""Settings model."""
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from dotenv import dotenv_values, set_key
|
|
6
6
|
from openbb_cli.config.constants import ENV_FILE_SETTINGS
|
|
7
|
+
from openbb_core.app.version import get_package_version
|
|
7
8
|
from pydantic import BaseModel, ConfigDict, model_validator
|
|
8
9
|
|
|
10
|
+
VERSION = get_package_version("openbb-cli")
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class Settings(BaseModel):
|
|
11
14
|
"""Settings model."""
|
|
12
15
|
|
|
13
16
|
# Platform CLI version
|
|
14
|
-
VERSION: str =
|
|
17
|
+
VERSION: str = VERSION
|
|
15
18
|
|
|
16
19
|
# DEVELOPMENT FLAGS
|
|
17
20
|
TEST_MODE: bool = False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "openbb-cli"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.2"
|
|
4
4
|
description = "Investment Research for Everyone, Anywhere."
|
|
5
5
|
authors = ["OpenBB <hello@openbb.co>"]
|
|
6
6
|
packages = [{ include = "openbb_cli" }]
|
|
@@ -17,16 +17,14 @@ openbb = 'openbb_cli.cli:main'
|
|
|
17
17
|
python = "^3.8.1,<3.12"
|
|
18
18
|
|
|
19
19
|
# OpenBB dependencies
|
|
20
|
-
openbb = { version = "^4.2.
|
|
20
|
+
openbb = { version = "^4.2.1", extras = ["all"] }
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# CLI dependencies
|
|
23
23
|
prompt-toolkit = "^3.0.16"
|
|
24
24
|
rich = "^13"
|
|
25
25
|
python-dotenv = "^1.0.0"
|
|
26
26
|
openpyxl = "^3.1.2"
|
|
27
|
-
|
|
28
|
-
[tool.poetry.group.dev.dependencies]
|
|
29
|
-
openbb-devtools = "^1.1.3"
|
|
27
|
+
openbb-core = "^1.2.4"
|
|
30
28
|
|
|
31
29
|
[build-system]
|
|
32
30
|
requires = ["setuptools<65.5.0", "poetry-core>=1.0.0"]
|
|
@@ -1,20 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|