Upload 70 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- cli/README.md +67 -0
- cli/integration/test_commands.py +29 -0
- cli/integration/test_integration_base_controller.py +89 -0
- cli/integration/test_integration_base_platform_controller.py +81 -0
- cli/integration/test_integration_cli_controller.py +26 -0
- cli/integration/test_integration_hub_service.py +62 -0
- cli/integration/test_integration_obbject_registry.py +54 -0
- cli/openbb_cli/__init__.py +1 -0
- cli/openbb_cli/argparse_translator/__init__.py +0 -0
- cli/openbb_cli/argparse_translator/argparse_argument.py +63 -0
- cli/openbb_cli/argparse_translator/argparse_class_processor.py +147 -0
- cli/openbb_cli/argparse_translator/argparse_translator.py +490 -0
- cli/openbb_cli/argparse_translator/obbject_registry.py +129 -0
- cli/openbb_cli/argparse_translator/reference_processor.py +142 -0
- cli/openbb_cli/argparse_translator/utils.py +76 -0
- cli/openbb_cli/assets/routines/routine_example.openbb +21 -0
- cli/openbb_cli/assets/styles/default/Consolas.ttf +3 -0
- cli/openbb_cli/assets/styles/default/dark.mpfstyle.json +47 -0
- cli/openbb_cli/assets/styles/default/dark.mplrc.json +7 -0
- cli/openbb_cli/assets/styles/default/dark.mplstyle +96 -0
- cli/openbb_cli/assets/styles/default/dark.pltstyle.json +132 -0
- cli/openbb_cli/assets/styles/default/dark.richstyle.json +9 -0
- cli/openbb_cli/assets/styles/default/light.mpfstyle.json +47 -0
- cli/openbb_cli/assets/styles/default/light.mplrc.json +7 -0
- cli/openbb_cli/assets/styles/default/light.mplstyle +95 -0
- cli/openbb_cli/assets/styles/default/light.pltstyle.json +871 -0
- cli/openbb_cli/assets/styles/default/light.richstyle.json +9 -0
- cli/openbb_cli/assets/styles/default/tables.pltstyle.json +102 -0
- cli/openbb_cli/assets/styles/user/openbb.richstyle.json +9 -0
- cli/openbb_cli/cli.py +31 -0
- cli/openbb_cli/config/__init__.py +1 -0
- cli/openbb_cli/config/completer.py +427 -0
- cli/openbb_cli/config/console.py +93 -0
- cli/openbb_cli/config/constants.py +80 -0
- cli/openbb_cli/config/menu_text.py +165 -0
- cli/openbb_cli/config/setup.py +11 -0
- cli/openbb_cli/config/style.py +108 -0
- cli/openbb_cli/controllers/base_controller.py +1032 -0
- cli/openbb_cli/controllers/base_platform_controller.py +392 -0
- cli/openbb_cli/controllers/choices.py +344 -0
- cli/openbb_cli/controllers/cli_controller.py +944 -0
- cli/openbb_cli/controllers/hub_service.py +107 -0
- cli/openbb_cli/controllers/platform_controller_factory.py +58 -0
- cli/openbb_cli/controllers/script_parser.py +488 -0
- cli/openbb_cli/controllers/settings_controller.py +142 -0
- cli/openbb_cli/controllers/utils.py +1032 -0
- cli/openbb_cli/models/settings.py +167 -0
- cli/openbb_cli/session.py +108 -0
- cli/openbb_cli/utils/utils.py +34 -0
.gitattributes
CHANGED
|
@@ -35,3 +35,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
build/conda/installer/assets/dmg_volume.icns filter=lfs diff=lfs merge=lfs -text
|
| 37 |
build/conda/installer/assets/Installer_vertical2.bmp filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
build/conda/installer/assets/dmg_volume.icns filter=lfs diff=lfs merge=lfs -text
|
| 37 |
build/conda/installer/assets/Installer_vertical2.bmp filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
cli/openbb_cli/assets/styles/default/Consolas.ttf filter=lfs diff=lfs merge=lfs -text
|
cli/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenBB Platform CLI
|
| 2 |
+
|
| 3 |
+
[](https://pepy.tech/project/openbb)
|
| 4 |
+
[](https://github.com/OpenBB-finance/OpenBB)
|
| 5 |
+
|
| 6 |
+
| OpenBB is committed to build the future of investment research by focusing on an open source infrastructure accessible to everyone, everywhere. |
|
| 7 |
+
| :---------------------------------------------------------------------------------------------------------------------------------------------: |
|
| 8 |
+
|  |
|
| 9 |
+
| Check our website at [openbb.co](www.openbb.co) |
|
| 10 |
+
|
| 11 |
+
## Overview
|
| 12 |
+
|
| 13 |
+
The OpenBB Platform CLI is a command line interface that wraps [OpenBB Platform](https://docs.openbb.co/platform).
|
| 14 |
+
|
| 15 |
+
It offers a convenient way to interact with the OpenBB Platform and its extensions, as well as automated data collection via OpenBB Routine Scripts.
|
| 16 |
+
|
| 17 |
+
Find the most complete documentation, examples, and usage guides for the OpenBB Platform CLI [here](https://docs.openbb.co/cli).
|
| 18 |
+
|
| 19 |
+
## Installation
|
| 20 |
+
|
| 21 |
+
The command below provides access to all the available OpenBB extensions behind the OpenBB Platform, find the complete list [here](https://my.openbb.co/app/platform/extensions).
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
pip install openbb-cli
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
> Note: Find the most complete installation hints and tips [here](https://docs.openbb.co/cli/installation).
|
| 28 |
+
|
| 29 |
+
After the installation is complete, you can deploy the OpenBB Platform CLI by running the following command:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
openbb
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
Which should result in the following output:
|
| 36 |
+
|
| 37 |
+

|
| 38 |
+
|
| 39 |
+
## API keys
|
| 40 |
+
|
| 41 |
+
To fully leverage the OpenBB Platform you need to get some API keys to connect with data providers. Here are the 3 options on where to set them:
|
| 42 |
+
|
| 43 |
+
1. OpenBB Hub
|
| 44 |
+
2. Local file
|
| 45 |
+
|
| 46 |
+
### 1. OpenBB Hub
|
| 47 |
+
|
| 48 |
+
Set your keys at [OpenBB Hub](https://my.openbb.co/app/platform/credentials) and get your personal access token from <https://my.openbb.co/app/platform/pat> to connect with your account.
|
| 49 |
+
|
| 50 |
+
> Once you log in, on the Platform CLI (through the `/account` menu, all your credentials will be in sync with the OpenBB Hub.)
|
| 51 |
+
|
| 52 |
+
### 2. Local file
|
| 53 |
+
|
| 54 |
+
You can specify the keys directly in the `~/.openbb_platform/user_settings.json` file.
|
| 55 |
+
|
| 56 |
+
Populate this file with the following template and replace the values with your keys:
|
| 57 |
+
|
| 58 |
+
```json
|
| 59 |
+
{
|
| 60 |
+
"credentials": {
|
| 61 |
+
"fmp_api_key": "REPLACE_ME",
|
| 62 |
+
"polygon_api_key": "REPLACE_ME",
|
| 63 |
+
"benzinga_api_key": "REPLACE_ME",
|
| 64 |
+
"fred_api_key": "REPLACE_ME"
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
```
|
cli/integration/test_commands.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from openbb_cli.cli import main
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@pytest.mark.parametrize(
|
| 8 |
+
"input_values",
|
| 9 |
+
[
|
| 10 |
+
"/equity/price/historical --symbol aapl --provider fmp",
|
| 11 |
+
"/equity/price/historical --symbol msft --provider yfinance",
|
| 12 |
+
"/equity/price/historical --symbol goog --provider polygon",
|
| 13 |
+
"/crypto/price/historical --symbol btc --provider fmp",
|
| 14 |
+
"/currency/price/historical --symbol eur --provider fmp",
|
| 15 |
+
"/derivatives/futures/historical --symbol cl --provider fmp",
|
| 16 |
+
"/etf/price/historical --symbol spy --provider fmp",
|
| 17 |
+
"/economy",
|
| 18 |
+
],
|
| 19 |
+
)
|
| 20 |
+
@pytest.mark.integration
|
| 21 |
+
def test_launch_with_cli_input(monkeypatch, input_values):
|
| 22 |
+
"""Test launching the CLI and providing input via stdin with multiple parameters."""
|
| 23 |
+
stdin = io.StringIO(input_values)
|
| 24 |
+
monkeypatch.setattr("sys.stdin", stdin)
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
main()
|
| 28 |
+
except Exception as e:
|
| 29 |
+
pytest.fail(f"Main function raised an exception: {e}")
|
cli/integration/test_integration_base_controller.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Integration tests for the base_controller module."""
|
| 2 |
+
|
| 3 |
+
from unittest.mock import Mock, patch
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
from openbb_cli.controllers.base_controller import BaseController
|
| 7 |
+
from openbb_cli.session import Session
|
| 8 |
+
|
| 9 |
+
# pylint: disable=unused-variable, redefined-outer-name
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class TestController(BaseController):
|
| 13 |
+
"""Test controller for the BaseController."""
|
| 14 |
+
|
| 15 |
+
PATH = "/test/"
|
| 16 |
+
|
| 17 |
+
def print_help(self):
|
| 18 |
+
"""Print help message."""
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@pytest.fixture
|
| 22 |
+
def base_controller():
|
| 23 |
+
"""Set up the environment for each test function."""
|
| 24 |
+
session = Session() # noqa: F841
|
| 25 |
+
controller = TestController()
|
| 26 |
+
return controller
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.mark.integration
|
| 30 |
+
def test_check_path_valid(base_controller):
|
| 31 |
+
"""Test that check_path does not raise an error for a valid path."""
|
| 32 |
+
base_controller.PATH = "/equity/"
|
| 33 |
+
try:
|
| 34 |
+
base_controller.check_path()
|
| 35 |
+
except ValueError:
|
| 36 |
+
pytest.fail("check_path raised ValueError unexpectedly!")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@pytest.mark.integration
|
| 40 |
+
def test_check_path_invalid(base_controller):
|
| 41 |
+
"""Test that check_path raises an error for an invalid path."""
|
| 42 |
+
with pytest.raises(ValueError):
|
| 43 |
+
base_controller.PATH = "invalid_path" # Missing leading '/'
|
| 44 |
+
base_controller.check_path()
|
| 45 |
+
|
| 46 |
+
with pytest.raises(ValueError):
|
| 47 |
+
base_controller.PATH = "/invalid_path" # Missing trailing '/'
|
| 48 |
+
base_controller.check_path()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@pytest.mark.integration
|
| 52 |
+
def test_parse_input(base_controller):
|
| 53 |
+
"""Test the parse_input method."""
|
| 54 |
+
input_str = "/equity/price/help"
|
| 55 |
+
expected_output = ["", "equity", "price", "help"]
|
| 56 |
+
assert (
|
| 57 |
+
base_controller.parse_input(input_str) == expected_output
|
| 58 |
+
), "Input parsing failed"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@pytest.mark.integration
|
| 62 |
+
def test_switch_command_execution(base_controller):
|
| 63 |
+
"""Test the switch method."""
|
| 64 |
+
base_controller.queue = []
|
| 65 |
+
base_controller.switch("/home/../reset/")
|
| 66 |
+
assert base_controller.queue == [
|
| 67 |
+
"home",
|
| 68 |
+
"..",
|
| 69 |
+
"reset",
|
| 70 |
+
], "Switch did not update the queue correctly"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@patch("openbb_cli.controllers.base_controller.BaseController.call_help")
|
| 74 |
+
@pytest.mark.integration
|
| 75 |
+
def test_command_routing(mock_call_help, base_controller):
|
| 76 |
+
"""Test the command routing."""
|
| 77 |
+
base_controller.switch("help")
|
| 78 |
+
mock_call_help.assert_called_once()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@pytest.mark.integration
|
| 82 |
+
def test_custom_reset(base_controller):
|
| 83 |
+
"""Test the custom reset method."""
|
| 84 |
+
base_controller.custom_reset = Mock(return_value=["custom", "reset"])
|
| 85 |
+
base_controller.call_reset(None)
|
| 86 |
+
expected_queue = ["quit", "reset", "custom", "reset"]
|
| 87 |
+
assert (
|
| 88 |
+
base_controller.queue == expected_queue
|
| 89 |
+
), f"Expected queue to be {expected_queue}, but was {base_controller.queue}"
|
cli/integration/test_integration_base_platform_controller.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test the base platform controller."""
|
| 2 |
+
|
| 3 |
+
from unittest.mock import MagicMock, Mock, patch
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
from openbb_cli.controllers.base_platform_controller import (
|
| 7 |
+
PlatformController,
|
| 8 |
+
Session,
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
# pylint: disable=protected-access, unused-variable, redefined-outer-name
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@pytest.fixture
|
| 15 |
+
def platform_controller():
|
| 16 |
+
"""Return a platform controller."""
|
| 17 |
+
session = Session() # noqa: F841
|
| 18 |
+
translators = {"test_command": MagicMock(), "test_menu": MagicMock()} # noqa: F841
|
| 19 |
+
translators["test_command"]._parser = Mock(
|
| 20 |
+
_actions=[Mock(dest="data", choices=[], type=str, nargs=None)]
|
| 21 |
+
)
|
| 22 |
+
translators["test_command"].execute_func = Mock(return_value=Mock())
|
| 23 |
+
translators["test_menu"]._parser = Mock(
|
| 24 |
+
_actions=[Mock(dest="data", choices=[], type=str, nargs=None)]
|
| 25 |
+
)
|
| 26 |
+
translators["test_menu"].execute_func = Mock(return_value=Mock())
|
| 27 |
+
|
| 28 |
+
controller = PlatformController(
|
| 29 |
+
name="test", parent_path=["platform"], translators=translators
|
| 30 |
+
)
|
| 31 |
+
return controller
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@pytest.mark.integration
|
| 35 |
+
def test_platform_controller_initialization(platform_controller):
|
| 36 |
+
"""Test the initialization of the platform controller."""
|
| 37 |
+
expected_path = "/platform/test/"
|
| 38 |
+
assert (
|
| 39 |
+
expected_path == platform_controller.PATH
|
| 40 |
+
), "Controller path was not set correctly"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@pytest.mark.integration
|
| 44 |
+
def test_command_generation(platform_controller):
|
| 45 |
+
"""Test the generation of commands."""
|
| 46 |
+
command_name = "test_command"
|
| 47 |
+
mock_execute_func = Mock(return_value=(Mock(), None))
|
| 48 |
+
platform_controller.translators[command_name].execute_func = mock_execute_func
|
| 49 |
+
|
| 50 |
+
platform_controller._generate_command_call(
|
| 51 |
+
name=command_name, translator=platform_controller.translators[command_name]
|
| 52 |
+
)
|
| 53 |
+
command_method_name = f"call_{command_name}"
|
| 54 |
+
assert hasattr(
|
| 55 |
+
platform_controller, command_method_name
|
| 56 |
+
), "Command method was not created"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@patch(
|
| 60 |
+
"openbb_cli.controllers.base_platform_controller.PlatformController._link_obbject_to_data_processing_commands"
|
| 61 |
+
)
|
| 62 |
+
@patch(
|
| 63 |
+
"openbb_cli.controllers.base_platform_controller.PlatformController._generate_commands"
|
| 64 |
+
)
|
| 65 |
+
@patch(
|
| 66 |
+
"openbb_cli.controllers.base_platform_controller.PlatformController._generate_sub_controllers"
|
| 67 |
+
)
|
| 68 |
+
@pytest.mark.integration
|
| 69 |
+
def test_platform_controller_calls(
|
| 70 |
+
mock_sub_controllers, mock_commands, mock_link_commands
|
| 71 |
+
):
|
| 72 |
+
"""Test the calls of the platform controller."""
|
| 73 |
+
translators = {"test_command": Mock()}
|
| 74 |
+
translators["test_command"].parser = Mock()
|
| 75 |
+
translators["test_command"].execute_func = Mock()
|
| 76 |
+
_ = PlatformController(
|
| 77 |
+
name="test", parent_path=["platform"], translators=translators
|
| 78 |
+
)
|
| 79 |
+
mock_sub_controllers.assert_called_once()
|
| 80 |
+
mock_commands.assert_called_once()
|
| 81 |
+
mock_link_commands.assert_called_once()
|
cli/integration/test_integration_cli_controller.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test the CLI controller integration."""
|
| 2 |
+
|
| 3 |
+
from openbb_cli.controllers.cli_controller import (
|
| 4 |
+
CLIController,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_parse_input_valid_commands():
|
| 9 |
+
"""Test parse_input method."""
|
| 10 |
+
controller = CLIController()
|
| 11 |
+
input_string = "exe --file test.openbb"
|
| 12 |
+
expected_output = [
|
| 13 |
+
"exe --file test.openbb"
|
| 14 |
+
] # Adjust based on actual expected behavior
|
| 15 |
+
assert controller.parse_input(input_string) == expected_output
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_parse_input_invalid_commands():
|
| 19 |
+
"""Test parse_input method."""
|
| 20 |
+
controller = CLIController()
|
| 21 |
+
input_string = "nonexistentcommand args"
|
| 22 |
+
expected_output = ["nonexistentcommand args"]
|
| 23 |
+
actual_output = controller.parse_input(input_string)
|
| 24 |
+
assert (
|
| 25 |
+
actual_output == expected_output
|
| 26 |
+
), f"Expected {expected_output}, got {actual_output}"
|
cli/integration/test_integration_hub_service.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Integration tests for the hub_service module."""
|
| 2 |
+
|
| 3 |
+
from unittest.mock import create_autospec, patch
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
import requests
|
| 7 |
+
from openbb_cli.controllers.hub_service import upload_routine
|
| 8 |
+
from openbb_core.app.model.hub.hub_session import HubSession
|
| 9 |
+
|
| 10 |
+
# pylint: disable=unused-argument, redefined-outer-name, unused-variable
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@pytest.fixture
|
| 14 |
+
def auth_header():
|
| 15 |
+
"""Return a fake auth header."""
|
| 16 |
+
return "Bearer fake_token"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@pytest.fixture
|
| 20 |
+
def hub_session_mock():
|
| 21 |
+
"""Return a mock HubSession."""
|
| 22 |
+
mock = create_autospec(HubSession, instance=True)
|
| 23 |
+
mock.username = "TestUser"
|
| 24 |
+
return mock
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# Fixture for routine data
|
| 28 |
+
@pytest.fixture
|
| 29 |
+
def routine_data():
|
| 30 |
+
"""Return a dictionary with routine data."""
|
| 31 |
+
return {
|
| 32 |
+
"name": "Test Routine",
|
| 33 |
+
"description": "A test routine",
|
| 34 |
+
"routine": "print('Hello World')",
|
| 35 |
+
"override": False,
|
| 36 |
+
"tags": "test",
|
| 37 |
+
"public": True,
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@pytest.mark.integration
|
| 42 |
+
def test_upload_routine_timeout(auth_header, routine_data):
|
| 43 |
+
"""Test upload_routine with a timeout exception."""
|
| 44 |
+
with patch(
|
| 45 |
+
"requests.post", side_effect=requests.exceptions.Timeout
|
| 46 |
+
) as mocked_post: # noqa: F841
|
| 47 |
+
|
| 48 |
+
response = upload_routine(auth_header, **routine_data)
|
| 49 |
+
|
| 50 |
+
assert response is None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@pytest.mark.integration
|
| 54 |
+
def test_upload_routine_connection_error(auth_header, routine_data):
|
| 55 |
+
"""Test upload_routine with a connection error."""
|
| 56 |
+
with patch(
|
| 57 |
+
"requests.post", side_effect=requests.exceptions.ConnectionError
|
| 58 |
+
) as mocked_post: # noqa: F841
|
| 59 |
+
|
| 60 |
+
response = upload_routine(auth_header, **routine_data)
|
| 61 |
+
|
| 62 |
+
assert response is None
|
cli/integration/test_integration_obbject_registry.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test the obbject registry."""
|
| 2 |
+
|
| 3 |
+
from openbb_cli.argparse_translator.obbject_registry import Registry
|
| 4 |
+
from openbb_core.app.model.obbject import OBBject
|
| 5 |
+
|
| 6 |
+
# pylint: disable=unused-variable
|
| 7 |
+
# ruff: noqa: disable=F841
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_registry_operations():
|
| 11 |
+
"""Test the registry operations."""
|
| 12 |
+
registry = Registry()
|
| 13 |
+
obbject1 = OBBject(
|
| 14 |
+
id="1", results=True, extra={"register_key": "key1", "command": "cmd1"}
|
| 15 |
+
)
|
| 16 |
+
obbject2 = OBBject(
|
| 17 |
+
id="2", results=True, extra={"register_key": "key2", "command": "cmd2"}
|
| 18 |
+
)
|
| 19 |
+
obbject3 = OBBject( # noqa: F841
|
| 20 |
+
id="3", results=True, extra={"register_key": "key3", "command": "cmd3"}
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# Add obbjects to the registry
|
| 24 |
+
assert registry.register(obbject1) is True
|
| 25 |
+
assert registry.register(obbject2) is True
|
| 26 |
+
# Attempt to add the same object again
|
| 27 |
+
assert registry.register(obbject1) is False
|
| 28 |
+
# Ensure the registry size is correct
|
| 29 |
+
assert len(registry.obbjects) == 2
|
| 30 |
+
|
| 31 |
+
# Get by index
|
| 32 |
+
assert registry.get(0) == obbject2
|
| 33 |
+
assert registry.get(1) == obbject1
|
| 34 |
+
# Get by key
|
| 35 |
+
assert registry.get("key1") == obbject1
|
| 36 |
+
assert registry.get("key2") == obbject2
|
| 37 |
+
# Invalid index/key
|
| 38 |
+
assert registry.get(2) is None
|
| 39 |
+
assert registry.get("invalid_key") is None
|
| 40 |
+
|
| 41 |
+
# Remove an object
|
| 42 |
+
registry.remove(0)
|
| 43 |
+
assert len(registry.obbjects) == 1
|
| 44 |
+
assert registry.get("key2") is None
|
| 45 |
+
|
| 46 |
+
# Validate the 'all' property
|
| 47 |
+
all_obbjects = registry.all
|
| 48 |
+
assert "command" in all_obbjects[0]
|
| 49 |
+
assert all_obbjects[0]["command"] == "cmd1"
|
| 50 |
+
|
| 51 |
+
# Clean up by removing all objects
|
| 52 |
+
registry.remove()
|
| 53 |
+
assert len(registry.obbjects) == 0
|
| 54 |
+
assert registry.get("key1") is None
|
cli/openbb_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Package init"""
|
cli/openbb_cli/argparse_translator/__init__.py
ADDED
|
File without changes
|
cli/openbb_cli/argparse_translator/argparse_argument.py
ADDED
|
@@ -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]
|
cli/openbb_cli/argparse_translator/argparse_class_processor.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for the ArgparseClassProcessor class."""
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
from typing import Any, Dict, Optional, Type
|
| 5 |
+
|
| 6 |
+
# TODO: this needs to be done differently
|
| 7 |
+
from openbb_core.app.static.container import Container
|
| 8 |
+
|
| 9 |
+
from openbb_cli.argparse_translator.argparse_translator import ArgparseTranslator
|
| 10 |
+
from openbb_cli.argparse_translator.reference_processor import (
|
| 11 |
+
ReferenceToArgumentsProcessor,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class ArgparseClassProcessor:
|
| 16 |
+
"""Process a target class to create ArgparseTranslators for its methods."""
|
| 17 |
+
|
| 18 |
+
# reference variable used to create custom groups for the ArgpaseTranslators
|
| 19 |
+
_reference: Dict[str, Any] = {}
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
target_class: Type,
|
| 24 |
+
add_help: bool = False,
|
| 25 |
+
reference: Optional[Dict[str, Any]] = None,
|
| 26 |
+
):
|
| 27 |
+
"""
|
| 28 |
+
Initialize the ArgparseClassProcessor.
|
| 29 |
+
|
| 30 |
+
Parameters
|
| 31 |
+
----------
|
| 32 |
+
target_class : Type
|
| 33 |
+
The target class whose methods will be processed.
|
| 34 |
+
add_help : Optional[bool]
|
| 35 |
+
Whether to add help to the ArgparseTranslators.
|
| 36 |
+
"""
|
| 37 |
+
self._target_class: Type = target_class
|
| 38 |
+
self._add_help: bool = add_help
|
| 39 |
+
self._translators: Dict[str, ArgparseTranslator] = {}
|
| 40 |
+
self._paths: Dict[str, str] = {}
|
| 41 |
+
|
| 42 |
+
ArgparseClassProcessor._reference = reference or {}
|
| 43 |
+
|
| 44 |
+
self._translators = self._process_class(
|
| 45 |
+
target=self._target_class, add_help=self._add_help
|
| 46 |
+
)
|
| 47 |
+
self._paths[self._get_class_name(self._target_class)] = "path"
|
| 48 |
+
self._build_paths(target=self._target_class)
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def translators(self) -> Dict[str, ArgparseTranslator]:
|
| 52 |
+
"""
|
| 53 |
+
Get the ArgparseTranslators associated with the target class.
|
| 54 |
+
|
| 55 |
+
Returns
|
| 56 |
+
-------
|
| 57 |
+
Dict[str, ArgparseTranslator]
|
| 58 |
+
The ArgparseTranslators associated with the target class.
|
| 59 |
+
"""
|
| 60 |
+
return self._translators
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def paths(self) -> Dict[str, str]:
|
| 64 |
+
"""
|
| 65 |
+
Get the paths associated with the target class.
|
| 66 |
+
|
| 67 |
+
Returns
|
| 68 |
+
-------
|
| 69 |
+
Dict[str, str]
|
| 70 |
+
The paths associated with the target class.
|
| 71 |
+
"""
|
| 72 |
+
return self._paths
|
| 73 |
+
|
| 74 |
+
@classmethod
|
| 75 |
+
def _custom_groups_from_reference(cls, class_name: str, function_name: str) -> Dict:
|
| 76 |
+
route = f"/{class_name.replace('_', '/')}/{function_name}"
|
| 77 |
+
reference = {route: cls._reference[route]} if route in cls._reference else {}
|
| 78 |
+
if not reference:
|
| 79 |
+
return {}
|
| 80 |
+
rp = ReferenceToArgumentsProcessor(reference)
|
| 81 |
+
return rp.custom_groups.get(route, {}) # type: ignore
|
| 82 |
+
|
| 83 |
+
@classmethod
|
| 84 |
+
def _process_class(
|
| 85 |
+
cls,
|
| 86 |
+
target: type,
|
| 87 |
+
add_help: bool = False,
|
| 88 |
+
) -> Dict[str, ArgparseTranslator]:
|
| 89 |
+
methods = {}
|
| 90 |
+
|
| 91 |
+
for name, member in inspect.getmembers(target):
|
| 92 |
+
if name.startswith("__") or name.startswith("_"):
|
| 93 |
+
continue
|
| 94 |
+
if inspect.ismethod(member):
|
| 95 |
+
class_name = cls._get_class_name(target)
|
| 96 |
+
methods[f"{class_name}_{name}"] = ArgparseTranslator(
|
| 97 |
+
func=member,
|
| 98 |
+
add_help=add_help,
|
| 99 |
+
custom_argument_groups=cls._custom_groups_from_reference( # type: ignore
|
| 100 |
+
class_name=class_name, function_name=name
|
| 101 |
+
),
|
| 102 |
+
)
|
| 103 |
+
elif isinstance(member, Container):
|
| 104 |
+
methods = {
|
| 105 |
+
**methods,
|
| 106 |
+
**cls._process_class(
|
| 107 |
+
target=getattr(target, name), add_help=add_help
|
| 108 |
+
),
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
return methods
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
def _get_class_name(target: type) -> str:
|
| 115 |
+
return (
|
| 116 |
+
str(type(target))
|
| 117 |
+
.rsplit(".", maxsplit=1)[-1]
|
| 118 |
+
.replace("'>", "")
|
| 119 |
+
.replace("ROUTER_", "")
|
| 120 |
+
.lower()
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
def get_translator(self, command: str) -> ArgparseTranslator:
|
| 124 |
+
"""
|
| 125 |
+
Retrieve the ArgparseTranslator object associated with a specific menu and command.
|
| 126 |
+
|
| 127 |
+
Parameters
|
| 128 |
+
----------
|
| 129 |
+
command : str
|
| 130 |
+
The command associated with the ArgparseTranslator.
|
| 131 |
+
|
| 132 |
+
Returns
|
| 133 |
+
-------
|
| 134 |
+
ArgparseTranslator
|
| 135 |
+
The ArgparseTranslator associated with the specified menu and command.
|
| 136 |
+
"""
|
| 137 |
+
return self._translators[command]
|
| 138 |
+
|
| 139 |
+
def _build_paths(self, target: type, depth: int = 1):
|
| 140 |
+
for name, member in inspect.getmembers(target):
|
| 141 |
+
if name.startswith("__") or name.startswith("_"):
|
| 142 |
+
continue
|
| 143 |
+
if inspect.ismethod(member):
|
| 144 |
+
pass
|
| 145 |
+
elif isinstance(member, Container):
|
| 146 |
+
self._build_paths(target=getattr(target, name), depth=depth + 1)
|
| 147 |
+
self._paths[f"{name}"] = "sub" * depth + "path"
|
cli/openbb_cli/argparse_translator/argparse_translator.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for translating a function into an argparse program."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import inspect
|
| 5 |
+
import re
|
| 6 |
+
from copy import deepcopy
|
| 7 |
+
from typing import (
|
| 8 |
+
Any,
|
| 9 |
+
Callable,
|
| 10 |
+
Dict,
|
| 11 |
+
List,
|
| 12 |
+
Literal,
|
| 13 |
+
Optional,
|
| 14 |
+
Tuple,
|
| 15 |
+
Type,
|
| 16 |
+
Union,
|
| 17 |
+
get_args,
|
| 18 |
+
get_origin,
|
| 19 |
+
get_type_hints,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
from openbb_core.app.model.field import OpenBBField
|
| 23 |
+
from pydantic import BaseModel
|
| 24 |
+
from typing_extensions import Annotated
|
| 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 |
+
|
| 38 |
+
# pylint: disable=protected-access
|
| 39 |
+
|
| 40 |
+
SEP = "__"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class ArgparseTranslator:
|
| 44 |
+
"""Class to translate a function into an argparse program."""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
func: Callable,
|
| 49 |
+
custom_argument_groups: Optional[List[ArgparseArgumentGroupModel]] = None,
|
| 50 |
+
add_help: Optional[bool] = True,
|
| 51 |
+
):
|
| 52 |
+
"""
|
| 53 |
+
Initialize the ArgparseTranslator.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
func (Callable): The function to translate into an argparse program.
|
| 57 |
+
add_help (Optional[bool], optional): Whether to add the help argument. Defaults to False.
|
| 58 |
+
"""
|
| 59 |
+
self.func = func
|
| 60 |
+
self.signature = inspect.signature(func)
|
| 61 |
+
self.type_hints = get_type_hints(func)
|
| 62 |
+
self.provider_parameters: Dict[str, List[str]] = {}
|
| 63 |
+
|
| 64 |
+
self._parser = argparse.ArgumentParser(
|
| 65 |
+
prog=func.__name__,
|
| 66 |
+
description=self._build_description(func.__doc__), # type: ignore
|
| 67 |
+
formatter_class=argparse.RawTextHelpFormatter,
|
| 68 |
+
add_help=add_help if add_help else False,
|
| 69 |
+
)
|
| 70 |
+
self._required = self._parser.add_argument_group("required arguments")
|
| 71 |
+
|
| 72 |
+
if any(param in self.type_hints for param in self.signature.parameters):
|
| 73 |
+
self._generate_argparse_arguments(self.signature.parameters)
|
| 74 |
+
|
| 75 |
+
if custom_argument_groups:
|
| 76 |
+
for group in custom_argument_groups:
|
| 77 |
+
self.provider_parameters[group.name] = []
|
| 78 |
+
argparse_group = self._parser.add_argument_group(group.name)
|
| 79 |
+
for argument in group.arguments:
|
| 80 |
+
self._handle_argument_in_groups(argument, argparse_group)
|
| 81 |
+
|
| 82 |
+
def _handle_argument_in_groups(self, argument, group):
|
| 83 |
+
"""Handle the argument and add it to the parser."""
|
| 84 |
+
|
| 85 |
+
def _update_providers(
|
| 86 |
+
input_string: str, new_provider: List[Optional[str]]
|
| 87 |
+
) -> str:
|
| 88 |
+
pattern = r"\(provider:\s*(.*?)\)"
|
| 89 |
+
providers = re.findall(pattern, input_string)
|
| 90 |
+
providers.extend(new_provider)
|
| 91 |
+
# remove pattern from help and add with new providers
|
| 92 |
+
input_string = re.sub(pattern, "", input_string).strip()
|
| 93 |
+
return f"{input_string} (provider: {', '.join(providers)})"
|
| 94 |
+
|
| 95 |
+
# check if the argument is already in use, if not, add it
|
| 96 |
+
if f"--{argument.name}" not in self._parser._option_string_actions:
|
| 97 |
+
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
|
| 98 |
+
group.add_argument(f"--{argument.name}", **kwargs)
|
| 99 |
+
if group.title in self.provider_parameters:
|
| 100 |
+
self.provider_parameters[group.title].append(argument.name)
|
| 101 |
+
|
| 102 |
+
else:
|
| 103 |
+
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
|
| 104 |
+
model_choices = kwargs.get("choices", ()) or ()
|
| 105 |
+
# extend choices
|
| 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)
|
| 109 |
+
|
| 110 |
+
# check if the argument is in the required arguments
|
| 111 |
+
if in_group(self._parser, argument.name, group_title="required arguments"):
|
| 112 |
+
for action in self._required._group_actions:
|
| 113 |
+
if action.dest == argument.name and choices:
|
| 114 |
+
# update choices
|
| 115 |
+
action.choices = choices
|
| 116 |
+
set_optional_choices(action, optional_choices)
|
| 117 |
+
return
|
| 118 |
+
|
| 119 |
+
# check if the argument is in the optional arguments
|
| 120 |
+
if in_group(self._parser, argument.name, group_title="optional arguments"):
|
| 121 |
+
for action in self._parser._actions:
|
| 122 |
+
if action.dest == argument.name:
|
| 123 |
+
# update choices
|
| 124 |
+
if choices:
|
| 125 |
+
action.choices = choices
|
| 126 |
+
set_optional_choices(action, optional_choices)
|
| 127 |
+
if argument.name not in self.signature.parameters:
|
| 128 |
+
# update help
|
| 129 |
+
action.help = _update_providers(
|
| 130 |
+
action.help or "", [group.title]
|
| 131 |
+
)
|
| 132 |
+
return
|
| 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 |
+
|
| 141 |
+
# if the argument is in use, remove it from all groups
|
| 142 |
+
# and return the groups that had the argument
|
| 143 |
+
groups_w_arg = remove_argument(self._parser, argument.name)
|
| 144 |
+
groups_w_arg.append(group.title) # add current group
|
| 145 |
+
|
| 146 |
+
# add it to the optional arguments group instead
|
| 147 |
+
if choices:
|
| 148 |
+
kwargs["choices"] = choices # update choices
|
| 149 |
+
# add provider info to the help
|
| 150 |
+
kwargs["help"] = _update_providers(argument.help or "", groups_w_arg)
|
| 151 |
+
action = self._parser.add_argument(f"--{argument.name}", **kwargs)
|
| 152 |
+
set_optional_choices(action, optional_choices)
|
| 153 |
+
|
| 154 |
+
@property
|
| 155 |
+
def parser(self) -> argparse.ArgumentParser:
|
| 156 |
+
"""Get the argparse parser."""
|
| 157 |
+
return deepcopy(self._parser)
|
| 158 |
+
|
| 159 |
+
@staticmethod
|
| 160 |
+
def _build_description(func_doc: str) -> str:
|
| 161 |
+
"""Build the description of the argparse program from the function docstring."""
|
| 162 |
+
patterns = ["openbb\n ======", "Parameters\n ----------"]
|
| 163 |
+
|
| 164 |
+
if func_doc:
|
| 165 |
+
for pattern in patterns:
|
| 166 |
+
if pattern in func_doc:
|
| 167 |
+
func_doc = func_doc[: func_doc.index(pattern)].strip()
|
| 168 |
+
break
|
| 169 |
+
|
| 170 |
+
return func_doc
|
| 171 |
+
|
| 172 |
+
@staticmethod
|
| 173 |
+
def _param_is_default(param: inspect.Parameter) -> bool:
|
| 174 |
+
"""Return True if the parameter has a default value."""
|
| 175 |
+
return param.default != inspect.Parameter.empty
|
| 176 |
+
|
| 177 |
+
def _get_action_type(self, param: inspect.Parameter) -> str:
|
| 178 |
+
"""Return the argparse action type for the given parameter."""
|
| 179 |
+
param_type = self.type_hints[param.name]
|
| 180 |
+
type_origin = get_origin(param_type)
|
| 181 |
+
if param_type is bool or (
|
| 182 |
+
type_origin is Union and bool in get_args(param_type)
|
| 183 |
+
):
|
| 184 |
+
return "store_true"
|
| 185 |
+
return "store"
|
| 186 |
+
|
| 187 |
+
def _get_type_and_choices( # noqa: PLR0912 # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
|
| 188 |
+
self, param: inspect.Parameter
|
| 189 |
+
) -> Tuple[Type[Any], Tuple[Any, ...]]:
|
| 190 |
+
"""Return the type and choices for the given parameter."""
|
| 191 |
+
param_type = self.type_hints[param.name]
|
| 192 |
+
type_origin = get_origin(param_type)
|
| 193 |
+
|
| 194 |
+
choices: tuple[Any, ...] = ()
|
| 195 |
+
|
| 196 |
+
if type_origin is Literal:
|
| 197 |
+
choices = get_args(param_type)
|
| 198 |
+
# Special handling for boolean literals
|
| 199 |
+
if all(isinstance(choice, bool) for choice in choices):
|
| 200 |
+
return bool, ()
|
| 201 |
+
param_type = type(choices[0]) # type: ignore
|
| 202 |
+
|
| 203 |
+
if type_origin is list:
|
| 204 |
+
param_type = get_args(param_type)[0]
|
| 205 |
+
|
| 206 |
+
if get_origin(param_type) is Literal:
|
| 207 |
+
choices = get_args(param_type)
|
| 208 |
+
# Special handling for boolean literals in lists
|
| 209 |
+
if all(isinstance(choice, bool) for choice in choices):
|
| 210 |
+
return bool, ()
|
| 211 |
+
param_type = type(choices[0]) # type: ignore
|
| 212 |
+
|
| 213 |
+
if type_origin is Union:
|
| 214 |
+
union_args = get_args(param_type)
|
| 215 |
+
# Check if Union contains bool type
|
| 216 |
+
if bool in union_args and (str in union_args or len(union_args) == 2):
|
| 217 |
+
return bool, ()
|
| 218 |
+
|
| 219 |
+
# Check if Union contains Literal types and extract all choices
|
| 220 |
+
literal_choices: list = []
|
| 221 |
+
for arg in union_args:
|
| 222 |
+
if get_origin(arg) is Literal:
|
| 223 |
+
literal_choices.extend(get_args(arg))
|
| 224 |
+
|
| 225 |
+
if literal_choices:
|
| 226 |
+
# Check if all choices are boolean
|
| 227 |
+
if all(isinstance(choice, bool) for choice in literal_choices):
|
| 228 |
+
return bool, ()
|
| 229 |
+
# We have Literal types in the Union, use their choices
|
| 230 |
+
choices = tuple(literal_choices)
|
| 231 |
+
param_type = type(choices[0]) # type: ignore
|
| 232 |
+
elif str in union_args:
|
| 233 |
+
param_type = str
|
| 234 |
+
|
| 235 |
+
# check if it's an Optional, which would be a Union with NoneType
|
| 236 |
+
if type(None) in get_args(param_type):
|
| 237 |
+
# remove NoneType from the args
|
| 238 |
+
args = [arg for arg in get_args(param_type) if arg is not None]
|
| 239 |
+
# if there is only one arg left, use it
|
| 240 |
+
if len(args) == 1:
|
| 241 |
+
param_type = args[0]
|
| 242 |
+
|
| 243 |
+
if get_origin(param_type) is Literal:
|
| 244 |
+
choices = get_args(param_type)
|
| 245 |
+
# Special handling for boolean literals
|
| 246 |
+
if all(isinstance(choice, bool) for choice in choices):
|
| 247 |
+
return bool, ()
|
| 248 |
+
param_type = type(choices[0]) # type: ignore
|
| 249 |
+
elif len(args) > 1:
|
| 250 |
+
# Handle Union with multiple types (not just Optional)
|
| 251 |
+
# Try to extract Literal types again from the filtered args
|
| 252 |
+
literal_choices = []
|
| 253 |
+
for arg in args:
|
| 254 |
+
if get_origin(arg) is Literal:
|
| 255 |
+
literal_choices.extend(get_args(arg))
|
| 256 |
+
|
| 257 |
+
if literal_choices:
|
| 258 |
+
# Check if all choices are boolean
|
| 259 |
+
if all(isinstance(choice, bool) for choice in literal_choices):
|
| 260 |
+
return bool, ()
|
| 261 |
+
choices = tuple(set(literal_choices))
|
| 262 |
+
param_type = type(choices[0]) # type: ignore
|
| 263 |
+
|
| 264 |
+
# if there are custom choices, override
|
| 265 |
+
custom_choices = self._get_argument_custom_choices(param)
|
| 266 |
+
if custom_choices and param_type is not bool:
|
| 267 |
+
choices = tuple(custom_choices)
|
| 268 |
+
|
| 269 |
+
return param_type, choices
|
| 270 |
+
|
| 271 |
+
@staticmethod
|
| 272 |
+
def _split_annotation(
|
| 273 |
+
base_annotation: Type[Any], custom_annotation_type: Type
|
| 274 |
+
) -> Tuple[Type[Any], List[Any]]:
|
| 275 |
+
"""Find the base annotation and the custom annotations, namely the OpenBBField."""
|
| 276 |
+
if get_origin(base_annotation) is not Annotated:
|
| 277 |
+
return base_annotation, []
|
| 278 |
+
base_annotation, *maybe_custom_annotations = get_args(base_annotation)
|
| 279 |
+
return base_annotation, [
|
| 280 |
+
annotation
|
| 281 |
+
for annotation in maybe_custom_annotations
|
| 282 |
+
if isinstance(annotation, custom_annotation_type)
|
| 283 |
+
]
|
| 284 |
+
|
| 285 |
+
@classmethod
|
| 286 |
+
def _get_argument_custom_help(cls, param: inspect.Parameter) -> Optional[str]:
|
| 287 |
+
"""Return the help annotation for the given parameter."""
|
| 288 |
+
base_annotation = param.annotation
|
| 289 |
+
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
|
| 290 |
+
help_annotation = (
|
| 291 |
+
custom_annotations[0].description if custom_annotations else None
|
| 292 |
+
)
|
| 293 |
+
return help_annotation
|
| 294 |
+
|
| 295 |
+
@classmethod
|
| 296 |
+
def _get_argument_custom_choices(cls, param: inspect.Parameter) -> Optional[str]:
|
| 297 |
+
"""Return the help annotation for the given parameter."""
|
| 298 |
+
base_annotation = param.annotation
|
| 299 |
+
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
|
| 300 |
+
choices_annotation = (
|
| 301 |
+
custom_annotations[0].choices if custom_annotations else None
|
| 302 |
+
)
|
| 303 |
+
return choices_annotation
|
| 304 |
+
|
| 305 |
+
def _get_nargs(self, param: inspect.Parameter) -> Optional[str]:
|
| 306 |
+
"""Return the nargs annotation for the given parameter."""
|
| 307 |
+
param_type = self.type_hints[param.name]
|
| 308 |
+
origin = get_origin(param_type)
|
| 309 |
+
|
| 310 |
+
if origin is list:
|
| 311 |
+
return "+"
|
| 312 |
+
|
| 313 |
+
if origin is Union and any(
|
| 314 |
+
get_origin(arg) is list for arg in get_args(param_type)
|
| 315 |
+
):
|
| 316 |
+
return "+"
|
| 317 |
+
|
| 318 |
+
return None
|
| 319 |
+
|
| 320 |
+
def _generate_argparse_arguments(self, parameters) -> None:
|
| 321 |
+
"""Generate the argparse arguments from the function parameters."""
|
| 322 |
+
for param in parameters.values():
|
| 323 |
+
if param.name == "kwargs":
|
| 324 |
+
continue
|
| 325 |
+
|
| 326 |
+
param_type, choices = self._get_type_and_choices(param)
|
| 327 |
+
|
| 328 |
+
# if the param is a custom type, we need to flatten it
|
| 329 |
+
if inspect.isclass(param_type) and issubclass(param_type, BaseModel):
|
| 330 |
+
# update type hints with the custom type fields
|
| 331 |
+
type_hints = get_type_hints(param_type)
|
| 332 |
+
# prefix the type hints keys with the param name
|
| 333 |
+
type_hints = {
|
| 334 |
+
f"{param.name}{SEP}{key}": value
|
| 335 |
+
for key, value in type_hints.items()
|
| 336 |
+
}
|
| 337 |
+
self.type_hints.update(type_hints)
|
| 338 |
+
# create a signature from the custom type
|
| 339 |
+
sig = inspect.signature(param_type)
|
| 340 |
+
|
| 341 |
+
# add help to the annotation
|
| 342 |
+
annotated_parameters: List[inspect.Parameter] = []
|
| 343 |
+
for child_param in sig.parameters.values():
|
| 344 |
+
new_child_param = child_param.replace(
|
| 345 |
+
name=f"{param.name}{SEP}{child_param.name}",
|
| 346 |
+
annotation=Annotated[
|
| 347 |
+
child_param.annotation,
|
| 348 |
+
OpenBBField(
|
| 349 |
+
description=param_type.model_json_schema()[
|
| 350 |
+
"properties"
|
| 351 |
+
][child_param.name].get("description", None)
|
| 352 |
+
),
|
| 353 |
+
],
|
| 354 |
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
| 355 |
+
)
|
| 356 |
+
annotated_parameters.append(new_child_param)
|
| 357 |
+
|
| 358 |
+
# replacing with the annotated parameters
|
| 359 |
+
new_signature = inspect.Signature(
|
| 360 |
+
parameters=annotated_parameters,
|
| 361 |
+
return_annotation=sig.return_annotation,
|
| 362 |
+
)
|
| 363 |
+
self._generate_argparse_arguments(new_signature.parameters)
|
| 364 |
+
|
| 365 |
+
# the custom type itself should not be added as an argument
|
| 366 |
+
continue
|
| 367 |
+
|
| 368 |
+
required = not self._param_is_default(param)
|
| 369 |
+
|
| 370 |
+
# Get the appropriate action based on the parameter type
|
| 371 |
+
action = self._get_action_type(param)
|
| 372 |
+
|
| 373 |
+
# For boolean parameters with action="store_true", we should not use any choices
|
| 374 |
+
if param_type is bool:
|
| 375 |
+
choices = ()
|
| 376 |
+
action = "store_true"
|
| 377 |
+
|
| 378 |
+
argument = ArgparseArgumentModel(
|
| 379 |
+
name=param.name,
|
| 380 |
+
type=param_type,
|
| 381 |
+
dest=param.name,
|
| 382 |
+
default=param.default,
|
| 383 |
+
required=required,
|
| 384 |
+
action=action,
|
| 385 |
+
help=self._get_argument_custom_help(param),
|
| 386 |
+
nargs=self._get_nargs(param),
|
| 387 |
+
choices=choices,
|
| 388 |
+
)
|
| 389 |
+
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
|
| 390 |
+
|
| 391 |
+
if required:
|
| 392 |
+
self._required.add_argument(
|
| 393 |
+
f"--{argument.name}",
|
| 394 |
+
**kwargs,
|
| 395 |
+
)
|
| 396 |
+
else:
|
| 397 |
+
self._parser.add_argument(
|
| 398 |
+
f"--{argument.name}",
|
| 399 |
+
**kwargs,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
@staticmethod
|
| 403 |
+
def _unflatten_args(args: dict) -> Dict[str, Any]:
|
| 404 |
+
"""Unflatten the args that were flattened by the custom types."""
|
| 405 |
+
result: Dict[str, Any] = {}
|
| 406 |
+
for key, value in args.items():
|
| 407 |
+
if SEP in key:
|
| 408 |
+
parts = key.split(SEP)
|
| 409 |
+
nested_dict = result
|
| 410 |
+
for part in parts[:-1]:
|
| 411 |
+
if part not in nested_dict:
|
| 412 |
+
nested_dict[part] = {}
|
| 413 |
+
nested_dict = nested_dict[part]
|
| 414 |
+
nested_dict[parts[-1]] = value
|
| 415 |
+
else:
|
| 416 |
+
result[key] = value
|
| 417 |
+
return result
|
| 418 |
+
|
| 419 |
+
def _update_with_custom_types(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
| 420 |
+
"""Update the kwargs with the custom types."""
|
| 421 |
+
# for each argument in the signature that is a custom type, we need to
|
| 422 |
+
# update the kwargs with the custom type kwargs
|
| 423 |
+
for param in self.signature.parameters.values():
|
| 424 |
+
if param.name == "kwargs":
|
| 425 |
+
continue
|
| 426 |
+
param_type, _ = self._get_type_and_choices(param)
|
| 427 |
+
if inspect.isclass(param_type) and issubclass(param_type, BaseModel):
|
| 428 |
+
custom_type_kwargs = kwargs[param.name]
|
| 429 |
+
kwargs[param.name] = param_type(**custom_type_kwargs)
|
| 430 |
+
|
| 431 |
+
return kwargs
|
| 432 |
+
|
| 433 |
+
def execute_func(
|
| 434 |
+
self,
|
| 435 |
+
parsed_args: Optional[argparse.Namespace] = None,
|
| 436 |
+
) -> Any:
|
| 437 |
+
"""
|
| 438 |
+
Execute the original function with the parsed arguments.
|
| 439 |
+
|
| 440 |
+
Args:
|
| 441 |
+
parsed_args (Optional[argparse.Namespace], optional): The parsed arguments. Defaults to None.
|
| 442 |
+
|
| 443 |
+
Returns:
|
| 444 |
+
Any: The return value of the original function.
|
| 445 |
+
|
| 446 |
+
"""
|
| 447 |
+
kwargs = self._unflatten_args(vars(parsed_args))
|
| 448 |
+
kwargs = self._update_with_custom_types(kwargs)
|
| 449 |
+
provider = kwargs.get("provider")
|
| 450 |
+
provider_args: List = []
|
| 451 |
+
if provider and provider in self.provider_parameters:
|
| 452 |
+
provider_args = self.provider_parameters[provider]
|
| 453 |
+
else:
|
| 454 |
+
for args in self.provider_parameters.values():
|
| 455 |
+
provider_args.extend(args)
|
| 456 |
+
|
| 457 |
+
# remove kwargs not matching the signature, provider parameters, or are empty.
|
| 458 |
+
kwargs = {
|
| 459 |
+
key: value
|
| 460 |
+
for key, value in kwargs.items()
|
| 461 |
+
if (
|
| 462 |
+
(key in self.signature.parameters or key in provider_args)
|
| 463 |
+
and (value or value is False)
|
| 464 |
+
)
|
| 465 |
+
}
|
| 466 |
+
return self.func(**kwargs)
|
| 467 |
+
|
| 468 |
+
def parse_args_and_execute(self) -> Any:
|
| 469 |
+
"""
|
| 470 |
+
Parse the arguments and executes the original function.
|
| 471 |
+
|
| 472 |
+
Returns:
|
| 473 |
+
Any: The return value of the original function.
|
| 474 |
+
"""
|
| 475 |
+
parsed_args = self._parser.parse_args()
|
| 476 |
+
|
| 477 |
+
return self.execute_func(parsed_args)
|
| 478 |
+
|
| 479 |
+
def translate(self) -> Callable:
|
| 480 |
+
"""
|
| 481 |
+
Wrap the original function with an argparse program.
|
| 482 |
+
|
| 483 |
+
Returns:
|
| 484 |
+
Callable: The original function wrapped with an argparse program.
|
| 485 |
+
"""
|
| 486 |
+
|
| 487 |
+
def wrapper_func():
|
| 488 |
+
return self.parse_args_and_execute()
|
| 489 |
+
|
| 490 |
+
return wrapper_func
|
cli/openbb_cli/argparse_translator/obbject_registry.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Registry for OBBjects."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from typing import Dict, List, Optional, Union
|
| 5 |
+
|
| 6 |
+
from openbb_core.app.model.obbject import OBBject
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Registry:
|
| 10 |
+
"""Registry for OBBjects."""
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
"""Initialize the registry."""
|
| 14 |
+
self._obbjects: List[OBBject] = []
|
| 15 |
+
|
| 16 |
+
@staticmethod
|
| 17 |
+
def _contains_obbject(uuid: str, obbjects: List[OBBject]) -> bool:
|
| 18 |
+
"""Check if obbject with uuid is in the registry."""
|
| 19 |
+
return any(obbject.id == uuid for obbject in obbjects)
|
| 20 |
+
|
| 21 |
+
def register(self, obbject: OBBject) -> bool:
|
| 22 |
+
"""Designed to add an OBBject instance to the registry."""
|
| 23 |
+
if (
|
| 24 |
+
isinstance(obbject, OBBject)
|
| 25 |
+
and not self._contains_obbject(obbject.id, self._obbjects)
|
| 26 |
+
and obbject.results
|
| 27 |
+
):
|
| 28 |
+
self._obbjects.append(obbject)
|
| 29 |
+
return True
|
| 30 |
+
return False
|
| 31 |
+
|
| 32 |
+
def get(self, arg: Union[int, str]) -> Optional[OBBject]:
|
| 33 |
+
"""Return the obbject with index or key."""
|
| 34 |
+
if isinstance(arg, int):
|
| 35 |
+
return self._get_by_index(arg)
|
| 36 |
+
if isinstance(arg, str):
|
| 37 |
+
return self._get_by_key(arg)
|
| 38 |
+
|
| 39 |
+
raise ValueError("Couldn't get the `OBBject` with the provided argument.")
|
| 40 |
+
|
| 41 |
+
def _get_by_key(self, key: str) -> Optional[OBBject]:
|
| 42 |
+
"""Return the obbject with key."""
|
| 43 |
+
for obbject in self._obbjects:
|
| 44 |
+
if obbject.extra.get("register_key", "") == key:
|
| 45 |
+
return obbject
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
def _get_by_index(self, idx: int) -> Optional[OBBject]:
|
| 49 |
+
"""Return the obbject at index idx."""
|
| 50 |
+
# the list should work as a stack
|
| 51 |
+
# i.e., the last element needs to be accessed by idx=0 and so on
|
| 52 |
+
reversed_list = list(reversed(self._obbjects))
|
| 53 |
+
|
| 54 |
+
# check if the index is out of bounds
|
| 55 |
+
if idx >= len(reversed_list):
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
return reversed_list[idx]
|
| 59 |
+
|
| 60 |
+
def remove(self, idx: int = -1):
|
| 61 |
+
"""Remove the obbject at index idx, default is the last element."""
|
| 62 |
+
# the list should work as a stack
|
| 63 |
+
# i.e., the last element needs to be accessed by idx=0 and so on
|
| 64 |
+
reversed_list = list(reversed(self._obbjects))
|
| 65 |
+
del reversed_list[idx]
|
| 66 |
+
self._obbjects = list(reversed(reversed_list))
|
| 67 |
+
|
| 68 |
+
@property
|
| 69 |
+
def all(self) -> Dict[int, Dict]:
|
| 70 |
+
"""Return all obbjects in the registry."""
|
| 71 |
+
|
| 72 |
+
def _handle_standard_params(obbject: OBBject) -> str:
|
| 73 |
+
"""Handle standard params for obbjects."""
|
| 74 |
+
standard_params_json = ""
|
| 75 |
+
std_params = getattr(
|
| 76 |
+
obbject, "_standard_params", {}
|
| 77 |
+
) # pylint: disable=protected-access
|
| 78 |
+
if std_params:
|
| 79 |
+
standard_params = {
|
| 80 |
+
k: str(v)[:30] for k, v in std_params.items() if v and k != "data"
|
| 81 |
+
}
|
| 82 |
+
standard_params_json = json.dumps(standard_params)
|
| 83 |
+
|
| 84 |
+
return standard_params_json
|
| 85 |
+
|
| 86 |
+
def _handle_data_repr(obbject: OBBject) -> str:
|
| 87 |
+
"""Handle data representation for obbjects."""
|
| 88 |
+
data_repr = ""
|
| 89 |
+
if hasattr(obbject, "results") and obbject.results:
|
| 90 |
+
data_schema = (
|
| 91 |
+
obbject.results[0].model_json_schema()
|
| 92 |
+
if obbject.results
|
| 93 |
+
and isinstance(obbject.results, list)
|
| 94 |
+
and hasattr(obbject.results[0], "model_json_schema")
|
| 95 |
+
else ""
|
| 96 |
+
)
|
| 97 |
+
if data_schema and "title" in data_schema:
|
| 98 |
+
data_repr = f"{data_schema['title']}" # type: ignore
|
| 99 |
+
if data_schema and "description" in data_schema:
|
| 100 |
+
data_repr += f" - {data_schema['description'].split('.')[0]}" # type: ignore
|
| 101 |
+
|
| 102 |
+
return data_repr
|
| 103 |
+
|
| 104 |
+
obbjects = {}
|
| 105 |
+
for i, obbject in enumerate(list(reversed(self._obbjects))):
|
| 106 |
+
obbjects[i] = {
|
| 107 |
+
"route": obbject._route, # pylint: disable=protected-access
|
| 108 |
+
"provider": obbject.provider,
|
| 109 |
+
"standard params": _handle_standard_params(obbject),
|
| 110 |
+
"data": _handle_data_repr(obbject),
|
| 111 |
+
"command": obbject.extra.get("command", ""),
|
| 112 |
+
"key": obbject.extra.get("register_key", ""),
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
return obbjects
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def obbjects(self) -> List[OBBject]:
|
| 119 |
+
"""Return all obbjects in the registry."""
|
| 120 |
+
return self._obbjects
|
| 121 |
+
|
| 122 |
+
@property
|
| 123 |
+
def obbject_keys(self) -> List[str]:
|
| 124 |
+
"""Return all obbject keys in the registry."""
|
| 125 |
+
return [
|
| 126 |
+
obbject.extra["register_key"]
|
| 127 |
+
for obbject in self._obbjects
|
| 128 |
+
if "register_key" in obbject.extra
|
| 129 |
+
]
|
cli/openbb_cli/argparse_translator/reference_processor.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for the ReferenceToArgumentsProcessor class."""
|
| 2 |
+
|
| 3 |
+
# `ForwardRef`needs to be imported because the usage of `eval()`,
|
| 4 |
+
# which creates a ForwardRef
|
| 5 |
+
# which would raise a not defined error if it's not imported here.
|
| 6 |
+
# pylint: disable=unused-import
|
| 7 |
+
from typing import (
|
| 8 |
+
Any,
|
| 9 |
+
Dict,
|
| 10 |
+
ForwardRef, # noqa: F401
|
| 11 |
+
List,
|
| 12 |
+
Literal,
|
| 13 |
+
Optional,
|
| 14 |
+
Tuple,
|
| 15 |
+
Union,
|
| 16 |
+
get_args,
|
| 17 |
+
get_origin,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
from openbb_cli.argparse_translator.argparse_argument import (
|
| 21 |
+
ArgparseArgumentGroupModel,
|
| 22 |
+
ArgparseArgumentModel,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ReferenceToArgumentsProcessor:
|
| 27 |
+
"""Class to process the reference and build custom argument groups."""
|
| 28 |
+
|
| 29 |
+
def __init__(self, reference: Dict[str, Dict]):
|
| 30 |
+
"""Initialize the ReferenceToArgumentsProcessor."""
|
| 31 |
+
self._reference = reference
|
| 32 |
+
self._custom_groups: Dict[str, List[ArgparseArgumentGroupModel]] = {}
|
| 33 |
+
|
| 34 |
+
self._build_custom_groups()
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def custom_groups(self) -> Dict[str, List[ArgparseArgumentGroupModel]]:
|
| 38 |
+
"""Get the custom groups."""
|
| 39 |
+
return self._custom_groups
|
| 40 |
+
|
| 41 |
+
@staticmethod
|
| 42 |
+
def _make_type_parsable(type_: str) -> type:
|
| 43 |
+
"""Make the type parsable by removing the annotations."""
|
| 44 |
+
if "Union" in type_ and "str" in type_:
|
| 45 |
+
return str
|
| 46 |
+
if "Union" in type_ and "int" in type_:
|
| 47 |
+
return int
|
| 48 |
+
if type_ in ["date", "datetime.time", "time"]:
|
| 49 |
+
return str
|
| 50 |
+
|
| 51 |
+
if any(x in type_ for x in ["gt=", "ge=", "lt=", "le="]):
|
| 52 |
+
if "Annotated" in type_:
|
| 53 |
+
type_ = type_.replace("Annotated[", "").replace("]", "")
|
| 54 |
+
type_ = type_.split(",")[0]
|
| 55 |
+
|
| 56 |
+
return eval(type_) # noqa: S307, E501 pylint: disable=eval-used
|
| 57 |
+
|
| 58 |
+
def _parse_type(self, type_: str) -> type:
|
| 59 |
+
"""Parse the type from the string representation."""
|
| 60 |
+
type_ = self._make_type_parsable(type_) # type: ignore
|
| 61 |
+
|
| 62 |
+
if get_origin(type_) is Literal:
|
| 63 |
+
type_ = type(get_args(type_)[0]) # type: ignore
|
| 64 |
+
|
| 65 |
+
return type_ # type: ignore
|
| 66 |
+
|
| 67 |
+
def _get_nargs(self, type_: type) -> Optional[Union[int, str]]:
|
| 68 |
+
"""Get the nargs for the given type."""
|
| 69 |
+
if get_origin(type_) is list:
|
| 70 |
+
return "+"
|
| 71 |
+
return None
|
| 72 |
+
|
| 73 |
+
def _get_choices(self, type_: str, custom_choices: Any) -> Tuple:
|
| 74 |
+
"""Get the choices for the given type."""
|
| 75 |
+
type_ = self._make_type_parsable(type_) # type: ignore
|
| 76 |
+
type_origin = get_origin(type_)
|
| 77 |
+
|
| 78 |
+
choices: tuple[Any, ...] = ()
|
| 79 |
+
|
| 80 |
+
if type_origin is Literal:
|
| 81 |
+
choices = get_args(type_)
|
| 82 |
+
|
| 83 |
+
if type_origin is list:
|
| 84 |
+
type_ = get_args(type_)[0]
|
| 85 |
+
|
| 86 |
+
if get_origin(type_) is Literal:
|
| 87 |
+
choices = get_args(type_)
|
| 88 |
+
|
| 89 |
+
if type_origin is Union and type(None) in get_args(type_):
|
| 90 |
+
# remove NoneType from the args
|
| 91 |
+
args = [arg for arg in get_args(type_) if arg != type(None)]
|
| 92 |
+
# if there is only one arg left, use it
|
| 93 |
+
if len(args) > 1:
|
| 94 |
+
raise ValueError("Union with NoneType should have only one type left")
|
| 95 |
+
type_ = args[0]
|
| 96 |
+
|
| 97 |
+
if get_origin(type_) is Literal:
|
| 98 |
+
choices = get_args(type_)
|
| 99 |
+
|
| 100 |
+
if custom_choices:
|
| 101 |
+
return tuple(custom_choices)
|
| 102 |
+
|
| 103 |
+
return choices
|
| 104 |
+
|
| 105 |
+
def _build_custom_groups(self):
|
| 106 |
+
"""Build the custom groups from the reference."""
|
| 107 |
+
for route, v in self._reference.items():
|
| 108 |
+
for provider, args in v["parameters"].items():
|
| 109 |
+
if provider == "standard":
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
custom_arguments = []
|
| 113 |
+
for arg in args:
|
| 114 |
+
if arg.get("standard"):
|
| 115 |
+
continue
|
| 116 |
+
|
| 117 |
+
type_ = self._parse_type(arg["type"])
|
| 118 |
+
|
| 119 |
+
custom_arguments.append(
|
| 120 |
+
ArgparseArgumentModel(
|
| 121 |
+
name=arg["name"],
|
| 122 |
+
type=type_,
|
| 123 |
+
dest=arg["name"],
|
| 124 |
+
default=arg["default"],
|
| 125 |
+
required=not (arg["optional"]),
|
| 126 |
+
action="store" if type_ != bool else "store_true",
|
| 127 |
+
help=arg["description"],
|
| 128 |
+
nargs=self._get_nargs(type_), # type: ignore
|
| 129 |
+
choices=self._get_choices(
|
| 130 |
+
arg["type"], custom_choices=arg["choices"]
|
| 131 |
+
),
|
| 132 |
+
)
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
group = ArgparseArgumentGroupModel(
|
| 136 |
+
name=provider, arguments=custom_arguments
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
if route not in self._custom_groups:
|
| 140 |
+
self._custom_groups[route] = []
|
| 141 |
+
|
| 142 |
+
self._custom_groups[route].append(group)
|
cli/openbb_cli/argparse_translator/utils.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 63 |
+
(opts and opts[0] == argument_name)
|
| 64 |
+
or action.dest == argument_name
|
| 65 |
+
and hasattr(action, "optional_choices")
|
| 66 |
+
):
|
| 67 |
+
return (
|
| 68 |
+
action.optional_choices # type: ignore[attr-defined] # this is a custom attribute
|
| 69 |
+
)
|
| 70 |
+
return False
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def set_optional_choices(action: Action, optional_choices: bool):
|
| 74 |
+
"""Set the optional_choices attribute of an action."""
|
| 75 |
+
if not hasattr(action, "optional_choices") and optional_choices:
|
| 76 |
+
setattr(action, "optional_choices", optional_choices)
|
cli/openbb_cli/assets/routines/routine_example.openbb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Go into the equity context
|
| 2 |
+
equity
|
| 3 |
+
|
| 4 |
+
# Get the company's profile
|
| 5 |
+
profile --symbol aapl
|
| 6 |
+
|
| 7 |
+
# Get company's statements
|
| 8 |
+
fundamental
|
| 9 |
+
balance --symbol aapl
|
| 10 |
+
cash --symbol aapl
|
| 11 |
+
transcript --symbol aapl --year 2023
|
| 12 |
+
|
| 13 |
+
# Load company's historical data
|
| 14 |
+
../price
|
| 15 |
+
historical --symbol aapl
|
| 16 |
+
|
| 17 |
+
# Get its performance
|
| 18 |
+
performance --symbol aapl
|
| 19 |
+
|
| 20 |
+
# Export the candle as an image and the historical data into a csv
|
| 21 |
+
historical --symbol aapl --chart --export csv,jpg
|
cli/openbb_cli/assets/styles/default/Consolas.ttf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:423504695a3de1f80c618e3e6ead215a6b891be06c179bf048bb5a80d5d0eda3
|
| 3 |
+
size 358460
|
cli/openbb_cli/assets/styles/default/dark.mpfstyle.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"style_name": "dark",
|
| 3 |
+
"base_mpf_style": null,
|
| 4 |
+
"marketcolors": {
|
| 5 |
+
"candle": {
|
| 6 |
+
"up": "#00ACFF",
|
| 7 |
+
"down": "#e4003a"
|
| 8 |
+
},
|
| 9 |
+
"edge": {
|
| 10 |
+
"up": "#00ACFF",
|
| 11 |
+
"down": "#e4003a"
|
| 12 |
+
},
|
| 13 |
+
"wick": {
|
| 14 |
+
"up": "#00ACFF",
|
| 15 |
+
"down": "#e4003a"
|
| 16 |
+
},
|
| 17 |
+
"ohlc": {
|
| 18 |
+
"up": "#00ACFF",
|
| 19 |
+
"down": "#e4003a"
|
| 20 |
+
},
|
| 21 |
+
"volume": {
|
| 22 |
+
"up": "#00ACFF",
|
| 23 |
+
"down": "#e4003a"
|
| 24 |
+
},
|
| 25 |
+
"vcedge": {
|
| 26 |
+
"up": "#219E4F",
|
| 27 |
+
"down": "#9E1711"
|
| 28 |
+
},
|
| 29 |
+
"vcdopcod": true,
|
| 30 |
+
"alpha": 1
|
| 31 |
+
},
|
| 32 |
+
"mavcolors": [
|
| 33 |
+
"#EB3DBC",
|
| 34 |
+
"#31EBEA",
|
| 35 |
+
"#EB8C54",
|
| 36 |
+
"#EB5549"
|
| 37 |
+
],
|
| 38 |
+
"y_on_right": true,
|
| 39 |
+
"gridcolor": "#A3A0A2",
|
| 40 |
+
"gridstyle": ":",
|
| 41 |
+
"facecolor": "black",
|
| 42 |
+
"edgecolor": null,
|
| 43 |
+
"figcolor": null,
|
| 44 |
+
"gridaxis": null,
|
| 45 |
+
"rc": null,
|
| 46 |
+
"legacy_rc": null
|
| 47 |
+
}
|
cli/openbb_cli/assets/styles/default/dark.mplrc.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"xticks_rotation": 10,
|
| 3 |
+
"tight_layout_padding": 2,
|
| 4 |
+
"pie_wedgeprops": {"linewidth": 0.5, "edgecolor": "#FFFFFF"},
|
| 5 |
+
"pie_startangle": 90,
|
| 6 |
+
"volume_bar_width": 0.5
|
| 7 |
+
}
|
cli/openbb_cli/assets/styles/default/dark.mplstyle
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The cool hackerman style
|
| 2 |
+
|
| 3 |
+
# LINES
|
| 4 |
+
# http://matplotlib.org/api/artist_api.html#module-matplotlib.lines
|
| 5 |
+
lines.linewidth: 1.5
|
| 6 |
+
lines.color: F5EFF3
|
| 7 |
+
lines.linestyle: -
|
| 8 |
+
lines.marker: None
|
| 9 |
+
lines.markeredgewidth: 0.5
|
| 10 |
+
lines.markersize: 4
|
| 11 |
+
lines.dash_joinstyle: miter
|
| 12 |
+
lines.dash_capstyle: butt
|
| 13 |
+
lines.solid_joinstyle: miter
|
| 14 |
+
lines.solid_capstyle: projecting
|
| 15 |
+
lines.antialiased: True
|
| 16 |
+
|
| 17 |
+
patch.edgecolor: F5EFF3
|
| 18 |
+
patch.facecolor: 00aaff
|
| 19 |
+
patch.linewidth: 1
|
| 20 |
+
|
| 21 |
+
# TEXT
|
| 22 |
+
# http://matplotlib.org/api/font_manager_api.html
|
| 23 |
+
font.family: sans-serif
|
| 24 |
+
font.sans-serif: Consolas, Deja Vu Sans, Arial, Helvetica
|
| 25 |
+
font.style: normal
|
| 26 |
+
font.variant: normal
|
| 27 |
+
font.weight: medium
|
| 28 |
+
font.stretch: normal
|
| 29 |
+
font.size: 16
|
| 30 |
+
|
| 31 |
+
text.color: F5EFF3
|
| 32 |
+
|
| 33 |
+
axes.spines.left: true
|
| 34 |
+
axes.spines.bottom: true
|
| 35 |
+
axes.spines.top: true
|
| 36 |
+
axes.spines.right: true
|
| 37 |
+
axes.linewidth: 0.4
|
| 38 |
+
|
| 39 |
+
axes.labelsize: small
|
| 40 |
+
axes.titlelocation: left
|
| 41 |
+
|
| 42 |
+
axes.facecolor: black
|
| 43 |
+
axes.edgecolor: F5EFF3
|
| 44 |
+
axes.labelcolor: F5EFF3
|
| 45 |
+
axes.grid: true
|
| 46 |
+
axes.grid.which: major
|
| 47 |
+
|
| 48 |
+
axes.prop_cycle: cycler('color', ['ffed00', 'ef7d00', 'e4003a', 'c13246', '822661', '48277c', '005ca9', '00aaff', '9b30d9', 'af005f', '5f00af', 'af87ff'])
|
| 49 |
+
|
| 50 |
+
xtick.color: F5EFF3
|
| 51 |
+
ytick.color: F5EFF3
|
| 52 |
+
|
| 53 |
+
xtick.major.size: 2
|
| 54 |
+
xtick.minor.size: 1
|
| 55 |
+
xtick.labelsize: small
|
| 56 |
+
xtick.alignment: center
|
| 57 |
+
xtick.major.width: 0.2
|
| 58 |
+
|
| 59 |
+
ytick.major.size: 2
|
| 60 |
+
ytick.minor.size: 1
|
| 61 |
+
ytick.labelsize: small
|
| 62 |
+
|
| 63 |
+
ytick.left: False
|
| 64 |
+
ytick.labelleft: False
|
| 65 |
+
ytick.right: True
|
| 66 |
+
ytick.labelright: True
|
| 67 |
+
ytick.major.width: 0.2
|
| 68 |
+
|
| 69 |
+
grid.color: F5EFF3
|
| 70 |
+
grid.linewidth: 0.4
|
| 71 |
+
grid.linestyle: :
|
| 72 |
+
|
| 73 |
+
legend.framealpha: 0.6
|
| 74 |
+
legend.frameon: true
|
| 75 |
+
legend.facecolor: black
|
| 76 |
+
legend.edgecolor: black
|
| 77 |
+
legend.scatterpoints: 3
|
| 78 |
+
legend.fontsize: small
|
| 79 |
+
legend.loc: upper left
|
| 80 |
+
|
| 81 |
+
figure.facecolor: black
|
| 82 |
+
figure.edgecolor: black
|
| 83 |
+
figure.subplot.hspace: 0.2
|
| 84 |
+
|
| 85 |
+
savefig.facecolor: black
|
| 86 |
+
savefig.edgecolor: black
|
| 87 |
+
|
| 88 |
+
### Boxplots
|
| 89 |
+
boxplot.boxprops.color: F5EFF3
|
| 90 |
+
boxplot.capprops.color: F5EFF3
|
| 91 |
+
boxplot.flierprops.color: F5EFF3
|
| 92 |
+
boxplot.flierprops.markeredgecolor: F5EFF3
|
| 93 |
+
boxplot.whiskerprops.color: F5EFF3
|
| 94 |
+
boxplot.medianprops.color: F5EFF3
|
| 95 |
+
boxplot.meanprops.markeredgecolor: F5EFF3
|
| 96 |
+
boxplot.meanprops.color: F5EFF3
|
cli/openbb_cli/assets/styles/default/dark.pltstyle.json
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"line": {
|
| 3 |
+
"up_color": "#00ACFF",
|
| 4 |
+
"down_color": "#e4003a",
|
| 5 |
+
"color": "#ffed00",
|
| 6 |
+
"width": 1.5
|
| 7 |
+
},
|
| 8 |
+
"data": {
|
| 9 |
+
"candlestick": [
|
| 10 |
+
{
|
| 11 |
+
"decreasing": {
|
| 12 |
+
"fillcolor": "#e4003a",
|
| 13 |
+
"line": {
|
| 14 |
+
"color": "#e4003a"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"increasing": {
|
| 18 |
+
"fillcolor": "#00ACFF",
|
| 19 |
+
"line": {
|
| 20 |
+
"color": "#00ACFF"
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"type": "candlestick"
|
| 24 |
+
}
|
| 25 |
+
]
|
| 26 |
+
},
|
| 27 |
+
"layout": {
|
| 28 |
+
"annotationdefaults": {
|
| 29 |
+
"showarrow": false
|
| 30 |
+
},
|
| 31 |
+
"autotypenumbers": "strict",
|
| 32 |
+
"colorway": [
|
| 33 |
+
"#ffed00",
|
| 34 |
+
"#ef7d00",
|
| 35 |
+
"#e4003a",
|
| 36 |
+
"#c13246",
|
| 37 |
+
"#822661",
|
| 38 |
+
"#48277c",
|
| 39 |
+
"#005ca9",
|
| 40 |
+
"#00aaff",
|
| 41 |
+
"#9b30d9",
|
| 42 |
+
"#af005f",
|
| 43 |
+
"#5f00af",
|
| 44 |
+
"#af87ff"
|
| 45 |
+
],
|
| 46 |
+
"dragmode": "pan",
|
| 47 |
+
"font": {
|
| 48 |
+
"family": "Fira Code",
|
| 49 |
+
"size": 18
|
| 50 |
+
},
|
| 51 |
+
"hoverlabel": {
|
| 52 |
+
"align": "left"
|
| 53 |
+
},
|
| 54 |
+
"mapbox": {
|
| 55 |
+
"style": "dark"
|
| 56 |
+
},
|
| 57 |
+
"hovermode": "x",
|
| 58 |
+
"legend": {
|
| 59 |
+
"bgcolor": "rgba(0, 0, 0, 0.5)",
|
| 60 |
+
"x": 0.01,
|
| 61 |
+
"xanchor": "left",
|
| 62 |
+
"y": 0.99,
|
| 63 |
+
"yanchor": "top",
|
| 64 |
+
"font": {
|
| 65 |
+
"size": 15
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"legend2": {
|
| 69 |
+
"bgcolor": "rgba(0, 0, 0, 0.5)",
|
| 70 |
+
"font": {
|
| 71 |
+
"size": 15
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"legend3": {
|
| 75 |
+
"bgcolor": "rgba(0, 0, 0, 0.5)",
|
| 76 |
+
"font": {
|
| 77 |
+
"size": 15
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"legend4": {
|
| 81 |
+
"bgcolor": "rgba(0, 0, 0, 0.5)",
|
| 82 |
+
"font": {
|
| 83 |
+
"size": 15
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"legend5": {
|
| 87 |
+
"bgcolor": "rgba(0, 0, 0, 0.5)",
|
| 88 |
+
"font": {
|
| 89 |
+
"size": 15
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"paper_bgcolor": "#000000",
|
| 93 |
+
"plot_bgcolor": "#000000",
|
| 94 |
+
"xaxis": {
|
| 95 |
+
"automargin": true,
|
| 96 |
+
"autorange": true,
|
| 97 |
+
"rangeslider": {
|
| 98 |
+
"visible": false
|
| 99 |
+
},
|
| 100 |
+
"showgrid": true,
|
| 101 |
+
"showline": true,
|
| 102 |
+
"tickfont": {
|
| 103 |
+
"size": 14
|
| 104 |
+
},
|
| 105 |
+
"zeroline": false,
|
| 106 |
+
"tick0": 1,
|
| 107 |
+
"title": {
|
| 108 |
+
"standoff": 20
|
| 109 |
+
},
|
| 110 |
+
"linecolor": "#F5EFF3",
|
| 111 |
+
"mirror": true,
|
| 112 |
+
"ticks": "outside"
|
| 113 |
+
},
|
| 114 |
+
"yaxis": {
|
| 115 |
+
"anchor": "x",
|
| 116 |
+
"automargin": true,
|
| 117 |
+
"fixedrange": false,
|
| 118 |
+
"zeroline": false,
|
| 119 |
+
"showgrid": true,
|
| 120 |
+
"showline": true,
|
| 121 |
+
"side": "right",
|
| 122 |
+
"tick0": 0.5,
|
| 123 |
+
"title": {
|
| 124 |
+
"standoff": 20
|
| 125 |
+
},
|
| 126 |
+
"gridcolor": "#283442",
|
| 127 |
+
"linecolor": "#F5EFF3",
|
| 128 |
+
"mirror": true,
|
| 129 |
+
"ticks": "outside"
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
}
|
cli/openbb_cli/assets/styles/default/dark.richstyle.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"info": "rgb(224,131,48)",
|
| 3 |
+
"cmds": "rgb(102,203,228)",
|
| 4 |
+
"param": "rgb(247,206,70)",
|
| 5 |
+
"menu": "rgb(50,115,185)",
|
| 6 |
+
"src": "rgb(216,90,64)",
|
| 7 |
+
"unvl": "grey30",
|
| 8 |
+
"help": "#FAC900"
|
| 9 |
+
}
|
cli/openbb_cli/assets/styles/default/light.mpfstyle.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"style_name": "light",
|
| 3 |
+
"base_mpf_style": null,
|
| 4 |
+
"marketcolors": {
|
| 5 |
+
"candle": {
|
| 6 |
+
"up": "#00ACFF",
|
| 7 |
+
"down": "#e4003a"
|
| 8 |
+
},
|
| 9 |
+
"edge": {
|
| 10 |
+
"up": "#00ACFF",
|
| 11 |
+
"down": "#e4003a"
|
| 12 |
+
},
|
| 13 |
+
"wick": {
|
| 14 |
+
"up": "#00ACFF",
|
| 15 |
+
"down": "#e4003a"
|
| 16 |
+
},
|
| 17 |
+
"ohlc": {
|
| 18 |
+
"up": "#00ACFF",
|
| 19 |
+
"down": "#e4003a"
|
| 20 |
+
},
|
| 21 |
+
"volume": {
|
| 22 |
+
"up": "#00ACFF",
|
| 23 |
+
"down": "#e4003a"
|
| 24 |
+
},
|
| 25 |
+
"vcedge": {
|
| 26 |
+
"up": "#219E4F",
|
| 27 |
+
"down": "#9E1711"
|
| 28 |
+
},
|
| 29 |
+
"vcdopcod": true,
|
| 30 |
+
"alpha": 1
|
| 31 |
+
},
|
| 32 |
+
"mavcolors": [
|
| 33 |
+
"#EB3DBC",
|
| 34 |
+
"#31EBEA",
|
| 35 |
+
"#EB8C54",
|
| 36 |
+
"#EB5549"
|
| 37 |
+
],
|
| 38 |
+
"y_on_right": true,
|
| 39 |
+
"gridcolor": "grey",
|
| 40 |
+
"gridstyle": ":",
|
| 41 |
+
"facecolor": "white",
|
| 42 |
+
"edgecolor": null,
|
| 43 |
+
"figcolor": null,
|
| 44 |
+
"gridaxis": null,
|
| 45 |
+
"rc": null,
|
| 46 |
+
"legacy_rc": null
|
| 47 |
+
}
|
cli/openbb_cli/assets/styles/default/light.mplrc.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"xticks_rotation": 10,
|
| 3 |
+
"tight_layout_padding": 2,
|
| 4 |
+
"pie_wedgeprops": {"linewidth": 0.5, "edgecolor": "#000000"},
|
| 5 |
+
"pie_startangle": 90,
|
| 6 |
+
"volume_bar_width": 0.5
|
| 7 |
+
}
|
cli/openbb_cli/assets/styles/default/light.mplstyle
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The cool hackerman style
|
| 2 |
+
|
| 3 |
+
# LINES
|
| 4 |
+
# http://matplotlib.org/api/artist_api.html#module-matplotlib.lines
|
| 5 |
+
lines.linewidth: 1.1
|
| 6 |
+
lines.color: 0F0F0F
|
| 7 |
+
lines.linestyle: -
|
| 8 |
+
lines.marker: None
|
| 9 |
+
lines.markeredgewidth: 0.5
|
| 10 |
+
lines.markersize: 4
|
| 11 |
+
lines.dash_joinstyle: miter
|
| 12 |
+
lines.dash_capstyle: butt
|
| 13 |
+
lines.solid_joinstyle: miter
|
| 14 |
+
lines.solid_capstyle: projecting
|
| 15 |
+
lines.antialiased: True
|
| 16 |
+
|
| 17 |
+
patch.edgecolor: 000000
|
| 18 |
+
patch.facecolor: 822661
|
| 19 |
+
patch.linewidth: 1
|
| 20 |
+
|
| 21 |
+
# TEXT
|
| 22 |
+
# http://matplotlib.org/api/font_manager_api.html
|
| 23 |
+
font.family: sans-serif
|
| 24 |
+
font.sans-serif: Consolas, Deja Vu Sans, Arial, Helvetica
|
| 25 |
+
font.style: normal
|
| 26 |
+
font.variant: normal
|
| 27 |
+
font.weight: medium
|
| 28 |
+
font.stretch: normal
|
| 29 |
+
font.size: 13
|
| 30 |
+
|
| 31 |
+
text.color: 0F0F0F
|
| 32 |
+
|
| 33 |
+
axes.spines.left: true
|
| 34 |
+
axes.spines.bottom: true
|
| 35 |
+
axes.spines.top: true
|
| 36 |
+
axes.spines.right: true
|
| 37 |
+
axes.linewidth: 0.2
|
| 38 |
+
|
| 39 |
+
axes.labelsize: small
|
| 40 |
+
axes.titlelocation: left
|
| 41 |
+
|
| 42 |
+
axes.facecolor: white
|
| 43 |
+
axes.edgecolor: 0F0F0F
|
| 44 |
+
axes.labelcolor: 0F0F0F
|
| 45 |
+
axes.grid: true
|
| 46 |
+
axes.grid.which: major
|
| 47 |
+
|
| 48 |
+
axes.prop_cycle: cycler('color', ['254495', 'c13246', '48277c', 'e4003a', 'ef7d00', '822661', 'ffed00', '00aaff', '9b30d9', 'af005f', '5f00af', 'af87ff'])
|
| 49 |
+
|
| 50 |
+
xtick.color: 0F0F0F
|
| 51 |
+
ytick.color: 0F0F0F
|
| 52 |
+
|
| 53 |
+
xtick.major.size: 2
|
| 54 |
+
xtick.minor.size: 1
|
| 55 |
+
xtick.labelsize: small
|
| 56 |
+
|
| 57 |
+
ytick.major.size: 2
|
| 58 |
+
ytick.minor.size: 1
|
| 59 |
+
ytick.labelsize: small
|
| 60 |
+
xtick.alignment: center
|
| 61 |
+
|
| 62 |
+
ytick.left: False
|
| 63 |
+
ytick.labelleft: False
|
| 64 |
+
ytick.right: True
|
| 65 |
+
ytick.labelright: True
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
grid.color: 0F0F0F
|
| 69 |
+
grid.linewidth: 0.4
|
| 70 |
+
grid.linestyle: :
|
| 71 |
+
|
| 72 |
+
legend.framealpha: 0.6
|
| 73 |
+
legend.frameon: true
|
| 74 |
+
legend.facecolor: white
|
| 75 |
+
legend.edgecolor: white
|
| 76 |
+
legend.scatterpoints: 3
|
| 77 |
+
legend.fontsize: small
|
| 78 |
+
legend.loc: best
|
| 79 |
+
|
| 80 |
+
figure.facecolor: white
|
| 81 |
+
figure.edgecolor: white
|
| 82 |
+
figure.subplot.hspace: 0.2
|
| 83 |
+
|
| 84 |
+
savefig.facecolor: white
|
| 85 |
+
savefig.edgecolor: white
|
| 86 |
+
|
| 87 |
+
### Boxplots
|
| 88 |
+
boxplot.boxprops.color: 0F0F0F
|
| 89 |
+
boxplot.capprops.color: 0F0F0F
|
| 90 |
+
boxplot.flierprops.color: 0F0F0F
|
| 91 |
+
boxplot.flierprops.markeredgecolor: 0F0F0F
|
| 92 |
+
boxplot.whiskerprops.color: 0F0F0F
|
| 93 |
+
boxplot.medianprops.color: 0F0F0F
|
| 94 |
+
boxplot.meanprops.markeredgecolor: 0F0F0F
|
| 95 |
+
boxplot.meanprops.color: 0F0F0F
|
cli/openbb_cli/assets/styles/default/light.pltstyle.json
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"line": {
|
| 3 |
+
"up_color": "#009600",
|
| 4 |
+
"down_color": "#c80000",
|
| 5 |
+
"color": "#0d0887",
|
| 6 |
+
"width": 1.5,
|
| 7 |
+
"down_color_transparent": "rgba(200, 0, 0, 0.4)",
|
| 8 |
+
"up_color_transparent": "rgba(0, 150, 0, 0.4)"
|
| 9 |
+
},
|
| 10 |
+
"data": {
|
| 11 |
+
"barpolar": [
|
| 12 |
+
{
|
| 13 |
+
"marker": {
|
| 14 |
+
"line": {
|
| 15 |
+
"color": "white",
|
| 16 |
+
"width": 0.5
|
| 17 |
+
},
|
| 18 |
+
"pattern": {
|
| 19 |
+
"fillmode": "overlay",
|
| 20 |
+
"size": 10,
|
| 21 |
+
"solidity": 0.2
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"type": "barpolar"
|
| 25 |
+
}
|
| 26 |
+
],
|
| 27 |
+
"bar": [
|
| 28 |
+
{
|
| 29 |
+
"error_x": {
|
| 30 |
+
"color": "#2a3f5f"
|
| 31 |
+
},
|
| 32 |
+
"error_y": {
|
| 33 |
+
"color": "#2a3f5f"
|
| 34 |
+
},
|
| 35 |
+
"marker": {
|
| 36 |
+
"line": {
|
| 37 |
+
"color": "white",
|
| 38 |
+
"width": 0.5
|
| 39 |
+
},
|
| 40 |
+
"pattern": {
|
| 41 |
+
"fillmode": "overlay",
|
| 42 |
+
"size": 10,
|
| 43 |
+
"solidity": 0.2
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"type": "bar"
|
| 47 |
+
}
|
| 48 |
+
],
|
| 49 |
+
"carpet": [
|
| 50 |
+
{
|
| 51 |
+
"aaxis": {
|
| 52 |
+
"endlinecolor": "#2a3f5f",
|
| 53 |
+
"gridcolor": "#C8D4E3",
|
| 54 |
+
"linecolor": "#C8D4E3",
|
| 55 |
+
"minorgridcolor": "#C8D4E3",
|
| 56 |
+
"startlinecolor": "#2a3f5f"
|
| 57 |
+
},
|
| 58 |
+
"baxis": {
|
| 59 |
+
"endlinecolor": "#2a3f5f",
|
| 60 |
+
"gridcolor": "#C8D4E3",
|
| 61 |
+
"linecolor": "#C8D4E3",
|
| 62 |
+
"minorgridcolor": "#C8D4E3",
|
| 63 |
+
"startlinecolor": "#2a3f5f"
|
| 64 |
+
},
|
| 65 |
+
"type": "carpet"
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"choropleth": [
|
| 69 |
+
{
|
| 70 |
+
"colorbar": {
|
| 71 |
+
"outlinewidth": 0,
|
| 72 |
+
"ticks": ""
|
| 73 |
+
},
|
| 74 |
+
"type": "choropleth"
|
| 75 |
+
}
|
| 76 |
+
],
|
| 77 |
+
"contourcarpet": [
|
| 78 |
+
{
|
| 79 |
+
"colorbar": {
|
| 80 |
+
"outlinewidth": 0,
|
| 81 |
+
"ticks": ""
|
| 82 |
+
},
|
| 83 |
+
"type": "contourcarpet"
|
| 84 |
+
}
|
| 85 |
+
],
|
| 86 |
+
"contour": [
|
| 87 |
+
{
|
| 88 |
+
"colorbar": {
|
| 89 |
+
"outlinewidth": 0,
|
| 90 |
+
"ticks": ""
|
| 91 |
+
},
|
| 92 |
+
"colorscale": [
|
| 93 |
+
[
|
| 94 |
+
0.0,
|
| 95 |
+
"#0d0887"
|
| 96 |
+
],
|
| 97 |
+
[
|
| 98 |
+
0.1111111111111111,
|
| 99 |
+
"#46039f"
|
| 100 |
+
],
|
| 101 |
+
[
|
| 102 |
+
0.2222222222222222,
|
| 103 |
+
"#7201a8"
|
| 104 |
+
],
|
| 105 |
+
[
|
| 106 |
+
0.3333333333333333,
|
| 107 |
+
"#9c179e"
|
| 108 |
+
],
|
| 109 |
+
[
|
| 110 |
+
0.4444444444444444,
|
| 111 |
+
"#bd3786"
|
| 112 |
+
],
|
| 113 |
+
[
|
| 114 |
+
0.5555555555555556,
|
| 115 |
+
"#d8576b"
|
| 116 |
+
],
|
| 117 |
+
[
|
| 118 |
+
0.6666666666666666,
|
| 119 |
+
"#ed7953"
|
| 120 |
+
],
|
| 121 |
+
[
|
| 122 |
+
0.7777777777777778,
|
| 123 |
+
"#fb9f3a"
|
| 124 |
+
],
|
| 125 |
+
[
|
| 126 |
+
0.8888888888888888,
|
| 127 |
+
"#fdca26"
|
| 128 |
+
],
|
| 129 |
+
[
|
| 130 |
+
1.0,
|
| 131 |
+
"#f0f921"
|
| 132 |
+
]
|
| 133 |
+
],
|
| 134 |
+
"type": "contour"
|
| 135 |
+
}
|
| 136 |
+
],
|
| 137 |
+
"heatmapgl": [
|
| 138 |
+
{
|
| 139 |
+
"colorbar": {
|
| 140 |
+
"outlinewidth": 0,
|
| 141 |
+
"ticks": ""
|
| 142 |
+
},
|
| 143 |
+
"colorscale": [
|
| 144 |
+
[
|
| 145 |
+
0.0,
|
| 146 |
+
"#0d0887"
|
| 147 |
+
],
|
| 148 |
+
[
|
| 149 |
+
0.1111111111111111,
|
| 150 |
+
"#46039f"
|
| 151 |
+
],
|
| 152 |
+
[
|
| 153 |
+
0.2222222222222222,
|
| 154 |
+
"#7201a8"
|
| 155 |
+
],
|
| 156 |
+
[
|
| 157 |
+
0.3333333333333333,
|
| 158 |
+
"#9c179e"
|
| 159 |
+
],
|
| 160 |
+
[
|
| 161 |
+
0.4444444444444444,
|
| 162 |
+
"#bd3786"
|
| 163 |
+
],
|
| 164 |
+
[
|
| 165 |
+
0.5555555555555556,
|
| 166 |
+
"#d8576b"
|
| 167 |
+
],
|
| 168 |
+
[
|
| 169 |
+
0.6666666666666666,
|
| 170 |
+
"#ed7953"
|
| 171 |
+
],
|
| 172 |
+
[
|
| 173 |
+
0.7777777777777778,
|
| 174 |
+
"#fb9f3a"
|
| 175 |
+
],
|
| 176 |
+
[
|
| 177 |
+
0.8888888888888888,
|
| 178 |
+
"#fdca26"
|
| 179 |
+
],
|
| 180 |
+
[
|
| 181 |
+
1.0,
|
| 182 |
+
"#f0f921"
|
| 183 |
+
]
|
| 184 |
+
],
|
| 185 |
+
"type": "heatmapgl"
|
| 186 |
+
}
|
| 187 |
+
],
|
| 188 |
+
"heatmap": [
|
| 189 |
+
{
|
| 190 |
+
"colorbar": {
|
| 191 |
+
"outlinewidth": 0,
|
| 192 |
+
"ticks": ""
|
| 193 |
+
},
|
| 194 |
+
"colorscale": [
|
| 195 |
+
[
|
| 196 |
+
0.0,
|
| 197 |
+
"#0d0887"
|
| 198 |
+
],
|
| 199 |
+
[
|
| 200 |
+
0.1111111111111111,
|
| 201 |
+
"#46039f"
|
| 202 |
+
],
|
| 203 |
+
[
|
| 204 |
+
0.2222222222222222,
|
| 205 |
+
"#7201a8"
|
| 206 |
+
],
|
| 207 |
+
[
|
| 208 |
+
0.3333333333333333,
|
| 209 |
+
"#9c179e"
|
| 210 |
+
],
|
| 211 |
+
[
|
| 212 |
+
0.4444444444444444,
|
| 213 |
+
"#bd3786"
|
| 214 |
+
],
|
| 215 |
+
[
|
| 216 |
+
0.5555555555555556,
|
| 217 |
+
"#d8576b"
|
| 218 |
+
],
|
| 219 |
+
[
|
| 220 |
+
0.6666666666666666,
|
| 221 |
+
"#ed7953"
|
| 222 |
+
],
|
| 223 |
+
[
|
| 224 |
+
0.7777777777777778,
|
| 225 |
+
"#fb9f3a"
|
| 226 |
+
],
|
| 227 |
+
[
|
| 228 |
+
0.8888888888888888,
|
| 229 |
+
"#fdca26"
|
| 230 |
+
],
|
| 231 |
+
[
|
| 232 |
+
1.0,
|
| 233 |
+
"#f0f921"
|
| 234 |
+
]
|
| 235 |
+
],
|
| 236 |
+
"type": "heatmap"
|
| 237 |
+
}
|
| 238 |
+
],
|
| 239 |
+
"histogram2dcontour": [
|
| 240 |
+
{
|
| 241 |
+
"colorbar": {
|
| 242 |
+
"outlinewidth": 0,
|
| 243 |
+
"ticks": ""
|
| 244 |
+
},
|
| 245 |
+
"colorscale": [
|
| 246 |
+
[
|
| 247 |
+
0.0,
|
| 248 |
+
"#0d0887"
|
| 249 |
+
],
|
| 250 |
+
[
|
| 251 |
+
0.1111111111111111,
|
| 252 |
+
"#46039f"
|
| 253 |
+
],
|
| 254 |
+
[
|
| 255 |
+
0.2222222222222222,
|
| 256 |
+
"#7201a8"
|
| 257 |
+
],
|
| 258 |
+
[
|
| 259 |
+
0.3333333333333333,
|
| 260 |
+
"#9c179e"
|
| 261 |
+
],
|
| 262 |
+
[
|
| 263 |
+
0.4444444444444444,
|
| 264 |
+
"#bd3786"
|
| 265 |
+
],
|
| 266 |
+
[
|
| 267 |
+
0.5555555555555556,
|
| 268 |
+
"#d8576b"
|
| 269 |
+
],
|
| 270 |
+
[
|
| 271 |
+
0.6666666666666666,
|
| 272 |
+
"#ed7953"
|
| 273 |
+
],
|
| 274 |
+
[
|
| 275 |
+
0.7777777777777778,
|
| 276 |
+
"#fb9f3a"
|
| 277 |
+
],
|
| 278 |
+
[
|
| 279 |
+
0.8888888888888888,
|
| 280 |
+
"#fdca26"
|
| 281 |
+
],
|
| 282 |
+
[
|
| 283 |
+
1.0,
|
| 284 |
+
"#f0f921"
|
| 285 |
+
]
|
| 286 |
+
],
|
| 287 |
+
"type": "histogram2dcontour"
|
| 288 |
+
}
|
| 289 |
+
],
|
| 290 |
+
"histogram2d": [
|
| 291 |
+
{
|
| 292 |
+
"colorbar": {
|
| 293 |
+
"outlinewidth": 0,
|
| 294 |
+
"ticks": ""
|
| 295 |
+
},
|
| 296 |
+
"colorscale": [
|
| 297 |
+
[
|
| 298 |
+
0.0,
|
| 299 |
+
"#0d0887"
|
| 300 |
+
],
|
| 301 |
+
[
|
| 302 |
+
0.1111111111111111,
|
| 303 |
+
"#46039f"
|
| 304 |
+
],
|
| 305 |
+
[
|
| 306 |
+
0.2222222222222222,
|
| 307 |
+
"#7201a8"
|
| 308 |
+
],
|
| 309 |
+
[
|
| 310 |
+
0.3333333333333333,
|
| 311 |
+
"#9c179e"
|
| 312 |
+
],
|
| 313 |
+
[
|
| 314 |
+
0.4444444444444444,
|
| 315 |
+
"#bd3786"
|
| 316 |
+
],
|
| 317 |
+
[
|
| 318 |
+
0.5555555555555556,
|
| 319 |
+
"#d8576b"
|
| 320 |
+
],
|
| 321 |
+
[
|
| 322 |
+
0.6666666666666666,
|
| 323 |
+
"#ed7953"
|
| 324 |
+
],
|
| 325 |
+
[
|
| 326 |
+
0.7777777777777778,
|
| 327 |
+
"#fb9f3a"
|
| 328 |
+
],
|
| 329 |
+
[
|
| 330 |
+
0.8888888888888888,
|
| 331 |
+
"#fdca26"
|
| 332 |
+
],
|
| 333 |
+
[
|
| 334 |
+
1.0,
|
| 335 |
+
"#f0f921"
|
| 336 |
+
]
|
| 337 |
+
],
|
| 338 |
+
"type": "histogram2d"
|
| 339 |
+
}
|
| 340 |
+
],
|
| 341 |
+
"histogram": [
|
| 342 |
+
{
|
| 343 |
+
"marker": {
|
| 344 |
+
"pattern": {
|
| 345 |
+
"fillmode": "overlay",
|
| 346 |
+
"size": 10,
|
| 347 |
+
"solidity": 0.2
|
| 348 |
+
}
|
| 349 |
+
},
|
| 350 |
+
"type": "histogram"
|
| 351 |
+
}
|
| 352 |
+
],
|
| 353 |
+
"mesh3d": [
|
| 354 |
+
{
|
| 355 |
+
"colorbar": {
|
| 356 |
+
"outlinewidth": 0,
|
| 357 |
+
"ticks": ""
|
| 358 |
+
},
|
| 359 |
+
"type": "mesh3d"
|
| 360 |
+
}
|
| 361 |
+
],
|
| 362 |
+
"parcoords": [
|
| 363 |
+
{
|
| 364 |
+
"line": {
|
| 365 |
+
"colorbar": {
|
| 366 |
+
"outlinewidth": 0,
|
| 367 |
+
"ticks": ""
|
| 368 |
+
}
|
| 369 |
+
},
|
| 370 |
+
"type": "parcoords"
|
| 371 |
+
}
|
| 372 |
+
],
|
| 373 |
+
"pie": [
|
| 374 |
+
{
|
| 375 |
+
"automargin": true,
|
| 376 |
+
"type": "pie"
|
| 377 |
+
}
|
| 378 |
+
],
|
| 379 |
+
"scatter3d": [
|
| 380 |
+
{
|
| 381 |
+
"line": {
|
| 382 |
+
"colorbar": {
|
| 383 |
+
"outlinewidth": 0,
|
| 384 |
+
"ticks": ""
|
| 385 |
+
}
|
| 386 |
+
},
|
| 387 |
+
"marker": {
|
| 388 |
+
"colorbar": {
|
| 389 |
+
"outlinewidth": 0,
|
| 390 |
+
"ticks": ""
|
| 391 |
+
}
|
| 392 |
+
},
|
| 393 |
+
"type": "scatter3d"
|
| 394 |
+
}
|
| 395 |
+
],
|
| 396 |
+
"scattercarpet": [
|
| 397 |
+
{
|
| 398 |
+
"marker": {
|
| 399 |
+
"colorbar": {
|
| 400 |
+
"outlinewidth": 0,
|
| 401 |
+
"ticks": ""
|
| 402 |
+
}
|
| 403 |
+
},
|
| 404 |
+
"type": "scattercarpet"
|
| 405 |
+
}
|
| 406 |
+
],
|
| 407 |
+
"scattergeo": [
|
| 408 |
+
{
|
| 409 |
+
"marker": {
|
| 410 |
+
"colorbar": {
|
| 411 |
+
"outlinewidth": 0,
|
| 412 |
+
"ticks": ""
|
| 413 |
+
}
|
| 414 |
+
},
|
| 415 |
+
"type": "scattergeo"
|
| 416 |
+
}
|
| 417 |
+
],
|
| 418 |
+
"scattergl": [
|
| 419 |
+
{
|
| 420 |
+
"marker": {
|
| 421 |
+
"colorbar": {
|
| 422 |
+
"outlinewidth": 0,
|
| 423 |
+
"ticks": ""
|
| 424 |
+
}
|
| 425 |
+
},
|
| 426 |
+
"type": "scattergl"
|
| 427 |
+
}
|
| 428 |
+
],
|
| 429 |
+
"scattermapbox": [
|
| 430 |
+
{
|
| 431 |
+
"marker": {
|
| 432 |
+
"colorbar": {
|
| 433 |
+
"outlinewidth": 0,
|
| 434 |
+
"ticks": ""
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"type": "scattermapbox"
|
| 438 |
+
}
|
| 439 |
+
],
|
| 440 |
+
"scatterpolargl": [
|
| 441 |
+
{
|
| 442 |
+
"marker": {
|
| 443 |
+
"colorbar": {
|
| 444 |
+
"outlinewidth": 0,
|
| 445 |
+
"ticks": ""
|
| 446 |
+
}
|
| 447 |
+
},
|
| 448 |
+
"type": "scatterpolargl"
|
| 449 |
+
}
|
| 450 |
+
],
|
| 451 |
+
"scatterpolar": [
|
| 452 |
+
{
|
| 453 |
+
"marker": {
|
| 454 |
+
"colorbar": {
|
| 455 |
+
"outlinewidth": 0,
|
| 456 |
+
"ticks": ""
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"type": "scatterpolar"
|
| 460 |
+
}
|
| 461 |
+
],
|
| 462 |
+
"scatter": [
|
| 463 |
+
{
|
| 464 |
+
"fillpattern": {
|
| 465 |
+
"fillmode": "overlay",
|
| 466 |
+
"size": 10,
|
| 467 |
+
"solidity": 0.2
|
| 468 |
+
},
|
| 469 |
+
"type": "scatter"
|
| 470 |
+
}
|
| 471 |
+
],
|
| 472 |
+
"scatterternary": [
|
| 473 |
+
{
|
| 474 |
+
"marker": {
|
| 475 |
+
"colorbar": {
|
| 476 |
+
"outlinewidth": 0,
|
| 477 |
+
"ticks": ""
|
| 478 |
+
}
|
| 479 |
+
},
|
| 480 |
+
"type": "scatterternary"
|
| 481 |
+
}
|
| 482 |
+
],
|
| 483 |
+
"surface": [
|
| 484 |
+
{
|
| 485 |
+
"colorbar": {
|
| 486 |
+
"outlinewidth": 0,
|
| 487 |
+
"ticks": ""
|
| 488 |
+
},
|
| 489 |
+
"colorscale": [
|
| 490 |
+
[
|
| 491 |
+
0.0,
|
| 492 |
+
"#0d0887"
|
| 493 |
+
],
|
| 494 |
+
[
|
| 495 |
+
0.1111111111111111,
|
| 496 |
+
"#46039f"
|
| 497 |
+
],
|
| 498 |
+
[
|
| 499 |
+
0.2222222222222222,
|
| 500 |
+
"#7201a8"
|
| 501 |
+
],
|
| 502 |
+
[
|
| 503 |
+
0.3333333333333333,
|
| 504 |
+
"#9c179e"
|
| 505 |
+
],
|
| 506 |
+
[
|
| 507 |
+
0.4444444444444444,
|
| 508 |
+
"#bd3786"
|
| 509 |
+
],
|
| 510 |
+
[
|
| 511 |
+
0.5555555555555556,
|
| 512 |
+
"#d8576b"
|
| 513 |
+
],
|
| 514 |
+
[
|
| 515 |
+
0.6666666666666666,
|
| 516 |
+
"#ed7953"
|
| 517 |
+
],
|
| 518 |
+
[
|
| 519 |
+
0.7777777777777778,
|
| 520 |
+
"#fb9f3a"
|
| 521 |
+
],
|
| 522 |
+
[
|
| 523 |
+
0.8888888888888888,
|
| 524 |
+
"#fdca26"
|
| 525 |
+
],
|
| 526 |
+
[
|
| 527 |
+
1.0,
|
| 528 |
+
"#f0f921"
|
| 529 |
+
]
|
| 530 |
+
],
|
| 531 |
+
"type": "surface"
|
| 532 |
+
}
|
| 533 |
+
],
|
| 534 |
+
"table": [
|
| 535 |
+
{
|
| 536 |
+
"cells": {
|
| 537 |
+
"fill": {
|
| 538 |
+
"color": "#EBF0F8"
|
| 539 |
+
},
|
| 540 |
+
"line": {
|
| 541 |
+
"color": "white"
|
| 542 |
+
}
|
| 543 |
+
},
|
| 544 |
+
"header": {
|
| 545 |
+
"fill": {
|
| 546 |
+
"color": "#C8D4E3"
|
| 547 |
+
},
|
| 548 |
+
"line": {
|
| 549 |
+
"color": "white"
|
| 550 |
+
}
|
| 551 |
+
},
|
| 552 |
+
"type": "table"
|
| 553 |
+
}
|
| 554 |
+
],
|
| 555 |
+
"candlestick": [
|
| 556 |
+
{
|
| 557 |
+
"decreasing": {
|
| 558 |
+
"fillcolor": "#c80000",
|
| 559 |
+
"line": {
|
| 560 |
+
"color": "#990000"
|
| 561 |
+
}
|
| 562 |
+
},
|
| 563 |
+
"increasing": {
|
| 564 |
+
"fillcolor": "#009600",
|
| 565 |
+
"line": {
|
| 566 |
+
"color": "#007500"
|
| 567 |
+
}
|
| 568 |
+
},
|
| 569 |
+
"type": "candlestick"
|
| 570 |
+
}
|
| 571 |
+
]
|
| 572 |
+
},
|
| 573 |
+
"layout": {
|
| 574 |
+
"annotationdefaults": {
|
| 575 |
+
"arrowcolor": "#2a3f5f",
|
| 576 |
+
"arrowhead": 0,
|
| 577 |
+
"arrowwidth": 1,
|
| 578 |
+
"showarrow": false
|
| 579 |
+
},
|
| 580 |
+
"autotypenumbers": "strict",
|
| 581 |
+
"coloraxis": {
|
| 582 |
+
"colorbar": {
|
| 583 |
+
"outlinewidth": 0,
|
| 584 |
+
"ticks": ""
|
| 585 |
+
}
|
| 586 |
+
},
|
| 587 |
+
"colorscale": {
|
| 588 |
+
"diverging": [
|
| 589 |
+
[
|
| 590 |
+
0,
|
| 591 |
+
"#8e0152"
|
| 592 |
+
],
|
| 593 |
+
[
|
| 594 |
+
0.1,
|
| 595 |
+
"#c51b7d"
|
| 596 |
+
],
|
| 597 |
+
[
|
| 598 |
+
0.2,
|
| 599 |
+
"#de77ae"
|
| 600 |
+
],
|
| 601 |
+
[
|
| 602 |
+
0.3,
|
| 603 |
+
"#f1b6da"
|
| 604 |
+
],
|
| 605 |
+
[
|
| 606 |
+
0.4,
|
| 607 |
+
"#fde0ef"
|
| 608 |
+
],
|
| 609 |
+
[
|
| 610 |
+
0.5,
|
| 611 |
+
"#f7f7f7"
|
| 612 |
+
],
|
| 613 |
+
[
|
| 614 |
+
0.6,
|
| 615 |
+
"#e6f5d0"
|
| 616 |
+
],
|
| 617 |
+
[
|
| 618 |
+
0.7,
|
| 619 |
+
"#b8e186"
|
| 620 |
+
],
|
| 621 |
+
[
|
| 622 |
+
0.8,
|
| 623 |
+
"#7fbc41"
|
| 624 |
+
],
|
| 625 |
+
[
|
| 626 |
+
0.9,
|
| 627 |
+
"#4d9221"
|
| 628 |
+
],
|
| 629 |
+
[
|
| 630 |
+
1,
|
| 631 |
+
"#276419"
|
| 632 |
+
]
|
| 633 |
+
],
|
| 634 |
+
"sequential": [
|
| 635 |
+
[
|
| 636 |
+
0.0,
|
| 637 |
+
"#0d0887"
|
| 638 |
+
],
|
| 639 |
+
[
|
| 640 |
+
0.1111111111111111,
|
| 641 |
+
"#46039f"
|
| 642 |
+
],
|
| 643 |
+
[
|
| 644 |
+
0.2222222222222222,
|
| 645 |
+
"#7201a8"
|
| 646 |
+
],
|
| 647 |
+
[
|
| 648 |
+
0.3333333333333333,
|
| 649 |
+
"#9c179e"
|
| 650 |
+
],
|
| 651 |
+
[
|
| 652 |
+
0.4444444444444444,
|
| 653 |
+
"#bd3786"
|
| 654 |
+
],
|
| 655 |
+
[
|
| 656 |
+
0.5555555555555556,
|
| 657 |
+
"#d8576b"
|
| 658 |
+
],
|
| 659 |
+
[
|
| 660 |
+
0.6666666666666666,
|
| 661 |
+
"#ed7953"
|
| 662 |
+
],
|
| 663 |
+
[
|
| 664 |
+
0.7777777777777778,
|
| 665 |
+
"#fb9f3a"
|
| 666 |
+
],
|
| 667 |
+
[
|
| 668 |
+
0.8888888888888888,
|
| 669 |
+
"#fdca26"
|
| 670 |
+
],
|
| 671 |
+
[
|
| 672 |
+
1.0,
|
| 673 |
+
"#f0f921"
|
| 674 |
+
]
|
| 675 |
+
],
|
| 676 |
+
"sequentialminus": [
|
| 677 |
+
[
|
| 678 |
+
0.0,
|
| 679 |
+
"#0d0887"
|
| 680 |
+
],
|
| 681 |
+
[
|
| 682 |
+
0.1111111111111111,
|
| 683 |
+
"#46039f"
|
| 684 |
+
],
|
| 685 |
+
[
|
| 686 |
+
0.2222222222222222,
|
| 687 |
+
"#7201a8"
|
| 688 |
+
],
|
| 689 |
+
[
|
| 690 |
+
0.3333333333333333,
|
| 691 |
+
"#9c179e"
|
| 692 |
+
],
|
| 693 |
+
[
|
| 694 |
+
0.4444444444444444,
|
| 695 |
+
"#bd3786"
|
| 696 |
+
],
|
| 697 |
+
[
|
| 698 |
+
0.5555555555555556,
|
| 699 |
+
"#d8576b"
|
| 700 |
+
],
|
| 701 |
+
[
|
| 702 |
+
0.6666666666666666,
|
| 703 |
+
"#ed7953"
|
| 704 |
+
],
|
| 705 |
+
[
|
| 706 |
+
0.7777777777777778,
|
| 707 |
+
"#fb9f3a"
|
| 708 |
+
],
|
| 709 |
+
[
|
| 710 |
+
0.8888888888888888,
|
| 711 |
+
"#fdca26"
|
| 712 |
+
],
|
| 713 |
+
[
|
| 714 |
+
1.0,
|
| 715 |
+
"#f0f921"
|
| 716 |
+
]
|
| 717 |
+
]
|
| 718 |
+
},
|
| 719 |
+
"colorway": [
|
| 720 |
+
"#254495",
|
| 721 |
+
"#c13246",
|
| 722 |
+
"#48277c",
|
| 723 |
+
"#e4003a",
|
| 724 |
+
"#ef7d00",
|
| 725 |
+
"#822661",
|
| 726 |
+
"#ffed00",
|
| 727 |
+
"#00aaff",
|
| 728 |
+
"#9b30d9",
|
| 729 |
+
"#af005f",
|
| 730 |
+
"#5f00af",
|
| 731 |
+
"#af87ff"
|
| 732 |
+
],
|
| 733 |
+
"font": {
|
| 734 |
+
"color": "#2a3f5f"
|
| 735 |
+
},
|
| 736 |
+
"geo": {
|
| 737 |
+
"bgcolor": "white",
|
| 738 |
+
"lakecolor": "white",
|
| 739 |
+
"landcolor": "white",
|
| 740 |
+
"showlakes": true,
|
| 741 |
+
"showland": true,
|
| 742 |
+
"subunitcolor": "#C8D4E3"
|
| 743 |
+
},
|
| 744 |
+
"hoverlabel": {
|
| 745 |
+
"align": "left"
|
| 746 |
+
},
|
| 747 |
+
"hovermode": "x",
|
| 748 |
+
"mapbox": {
|
| 749 |
+
"style": "light"
|
| 750 |
+
},
|
| 751 |
+
"paper_bgcolor": "white",
|
| 752 |
+
"plot_bgcolor": "white",
|
| 753 |
+
"polar": {
|
| 754 |
+
"angularaxis": {
|
| 755 |
+
"gridcolor": "#EBF0F8",
|
| 756 |
+
"linecolor": "#EBF0F8",
|
| 757 |
+
"ticks": ""
|
| 758 |
+
},
|
| 759 |
+
"bgcolor": "white",
|
| 760 |
+
"radialaxis": {
|
| 761 |
+
"gridcolor": "#EBF0F8",
|
| 762 |
+
"linecolor": "#EBF0F8",
|
| 763 |
+
"ticks": ""
|
| 764 |
+
}
|
| 765 |
+
},
|
| 766 |
+
"scene": {
|
| 767 |
+
"xaxis": {
|
| 768 |
+
"backgroundcolor": "white",
|
| 769 |
+
"gridcolor": "#DFE8F3",
|
| 770 |
+
"gridwidth": 2,
|
| 771 |
+
"linecolor": "#EBF0F8",
|
| 772 |
+
"showbackground": true,
|
| 773 |
+
"ticks": "",
|
| 774 |
+
"zerolinecolor": "#EBF0F8"
|
| 775 |
+
},
|
| 776 |
+
"yaxis": {
|
| 777 |
+
"backgroundcolor": "white",
|
| 778 |
+
"gridcolor": "#DFE8F3",
|
| 779 |
+
"gridwidth": 2,
|
| 780 |
+
"linecolor": "#EBF0F8",
|
| 781 |
+
"showbackground": true,
|
| 782 |
+
"ticks": "",
|
| 783 |
+
"zerolinecolor": "#EBF0F8"
|
| 784 |
+
},
|
| 785 |
+
"zaxis": {
|
| 786 |
+
"backgroundcolor": "white",
|
| 787 |
+
"gridcolor": "#DFE8F3",
|
| 788 |
+
"gridwidth": 2,
|
| 789 |
+
"linecolor": "#EBF0F8",
|
| 790 |
+
"showbackground": true,
|
| 791 |
+
"ticks": "",
|
| 792 |
+
"zerolinecolor": "#EBF0F8"
|
| 793 |
+
}
|
| 794 |
+
},
|
| 795 |
+
"shapedefaults": {
|
| 796 |
+
"line": {
|
| 797 |
+
"color": "#2a3f5f"
|
| 798 |
+
}
|
| 799 |
+
},
|
| 800 |
+
"ternary": {
|
| 801 |
+
"aaxis": {
|
| 802 |
+
"gridcolor": "#DFE8F3",
|
| 803 |
+
"linecolor": "#A2B1C6",
|
| 804 |
+
"ticks": ""
|
| 805 |
+
},
|
| 806 |
+
"baxis": {
|
| 807 |
+
"gridcolor": "#DFE8F3",
|
| 808 |
+
"linecolor": "#A2B1C6",
|
| 809 |
+
"ticks": ""
|
| 810 |
+
},
|
| 811 |
+
"bgcolor": "white",
|
| 812 |
+
"caxis": {
|
| 813 |
+
"gridcolor": "#DFE8F3",
|
| 814 |
+
"linecolor": "#A2B1C6",
|
| 815 |
+
"ticks": ""
|
| 816 |
+
}
|
| 817 |
+
},
|
| 818 |
+
"title": {
|
| 819 |
+
"x": 0.05
|
| 820 |
+
},
|
| 821 |
+
"xaxis": {
|
| 822 |
+
"automargin": true,
|
| 823 |
+
"ticks": "",
|
| 824 |
+
"zerolinewidth": 2,
|
| 825 |
+
"rangeslider": {
|
| 826 |
+
"visible": false
|
| 827 |
+
},
|
| 828 |
+
"showgrid": true,
|
| 829 |
+
"showline": true,
|
| 830 |
+
"tickfont": {
|
| 831 |
+
"size": 15
|
| 832 |
+
},
|
| 833 |
+
"mirror": true,
|
| 834 |
+
"zeroline": false
|
| 835 |
+
},
|
| 836 |
+
"yaxis": {
|
| 837 |
+
"automargin": true,
|
| 838 |
+
"ticks": "",
|
| 839 |
+
"tickfont": {
|
| 840 |
+
"size": 15
|
| 841 |
+
},
|
| 842 |
+
"zerolinewidth": 2,
|
| 843 |
+
"fixedrange": false,
|
| 844 |
+
"showgrid": true,
|
| 845 |
+
"showline": true,
|
| 846 |
+
"side": "right",
|
| 847 |
+
"mirror": true,
|
| 848 |
+
"zeroline": false
|
| 849 |
+
},
|
| 850 |
+
"dragmode": "pan",
|
| 851 |
+
"legend": {
|
| 852 |
+
"bgcolor": "rgba(0, 0, 0, 0)",
|
| 853 |
+
"x": 1.1,
|
| 854 |
+
"xanchor": "left",
|
| 855 |
+
"y": 0.99,
|
| 856 |
+
"yanchor": "top"
|
| 857 |
+
},
|
| 858 |
+
"legend2": {
|
| 859 |
+
"bgcolor": "rgba(0, 0, 0, 0)"
|
| 860 |
+
},
|
| 861 |
+
"legend3": {
|
| 862 |
+
"bgcolor": "rgba(0, 0, 0, 0)"
|
| 863 |
+
},
|
| 864 |
+
"legend4": {
|
| 865 |
+
"bgcolor": "rgba(0, 0, 0, 0)"
|
| 866 |
+
},
|
| 867 |
+
"legend5": {
|
| 868 |
+
"bgcolor": "rgba(0, 0, 0, 0)"
|
| 869 |
+
}
|
| 870 |
+
}
|
| 871 |
+
}
|
cli/openbb_cli/assets/styles/default/light.richstyle.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"info": "rgb(224,131,48)",
|
| 3 |
+
"cmds": "rgb(70,156,222)",
|
| 4 |
+
"param": "rgb(247,206,70)",
|
| 5 |
+
"menu": "rgb(50,115,185)",
|
| 6 |
+
"src": "rgb(216,90,64)",
|
| 7 |
+
"unvl": "grey30",
|
| 8 |
+
"help": "#FAC900"
|
| 9 |
+
}
|
cli/openbb_cli/assets/styles/default/tables.pltstyle.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"data": {
|
| 3 |
+
"candlestick": [
|
| 4 |
+
{
|
| 5 |
+
"decreasing": {
|
| 6 |
+
"fillcolor": "#e4003a",
|
| 7 |
+
"line": {
|
| 8 |
+
"color": "#e4003a"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"increasing": {
|
| 12 |
+
"fillcolor": "#00ACFF",
|
| 13 |
+
"line": {
|
| 14 |
+
"color": "#00ACFF"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"type": "candlestick"
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
},
|
| 21 |
+
"layout": {
|
| 22 |
+
"annotationdefaults": {
|
| 23 |
+
"showarrow": false
|
| 24 |
+
},
|
| 25 |
+
"autotypenumbers": "strict",
|
| 26 |
+
"colorway": [
|
| 27 |
+
"#ffed00",
|
| 28 |
+
"#ef7d00",
|
| 29 |
+
"#e4003a",
|
| 30 |
+
"#c13246",
|
| 31 |
+
"#822661",
|
| 32 |
+
"#48277c",
|
| 33 |
+
"#005ca9",
|
| 34 |
+
"#00aaff",
|
| 35 |
+
"#9b30d9",
|
| 36 |
+
"#af005f",
|
| 37 |
+
"#5f00af",
|
| 38 |
+
"#af87ff"
|
| 39 |
+
],
|
| 40 |
+
"dragmode": "pan",
|
| 41 |
+
"font": {
|
| 42 |
+
"family": "Fira Code",
|
| 43 |
+
"size": 18
|
| 44 |
+
},
|
| 45 |
+
"hoverlabel": {
|
| 46 |
+
"align": "left"
|
| 47 |
+
},
|
| 48 |
+
"mapbox": {
|
| 49 |
+
"style": "light"
|
| 50 |
+
},
|
| 51 |
+
"hovermode": "x",
|
| 52 |
+
"legend": {
|
| 53 |
+
"bgcolor": "rgba(0, 0, 0, 0)",
|
| 54 |
+
"x": 0.01,
|
| 55 |
+
"xanchor": "left",
|
| 56 |
+
"y": 0.99,
|
| 57 |
+
"yanchor": "top",
|
| 58 |
+
"font": {
|
| 59 |
+
"size": 15
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"paper_bgcolor": "white",
|
| 63 |
+
"plot_bgcolor": "white",
|
| 64 |
+
"xaxis": {
|
| 65 |
+
"automargin": true,
|
| 66 |
+
"autorange": true,
|
| 67 |
+
"rangeslider": {
|
| 68 |
+
"visible": false
|
| 69 |
+
},
|
| 70 |
+
"showgrid": true,
|
| 71 |
+
"showline": true,
|
| 72 |
+
"tickfont": {
|
| 73 |
+
"size": 14
|
| 74 |
+
},
|
| 75 |
+
"zeroline": false,
|
| 76 |
+
"tick0": 1,
|
| 77 |
+
"title": {
|
| 78 |
+
"standoff": 20
|
| 79 |
+
},
|
| 80 |
+
"linecolor": "#F5EFF3",
|
| 81 |
+
"mirror": true,
|
| 82 |
+
"ticks": "outside"
|
| 83 |
+
},
|
| 84 |
+
"yaxis": {
|
| 85 |
+
"anchor": "x",
|
| 86 |
+
"automargin": true,
|
| 87 |
+
"fixedrange": false,
|
| 88 |
+
"zeroline": false,
|
| 89 |
+
"showgrid": true,
|
| 90 |
+
"showline": true,
|
| 91 |
+
"side": "right",
|
| 92 |
+
"tick0": 0.5,
|
| 93 |
+
"title": {
|
| 94 |
+
"standoff": 20
|
| 95 |
+
},
|
| 96 |
+
"gridcolor": "#283442",
|
| 97 |
+
"linecolor": "#F5EFF3",
|
| 98 |
+
"mirror": true,
|
| 99 |
+
"ticks": "outside"
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
cli/openbb_cli/assets/styles/user/openbb.richstyle.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"info": "rgb(224,131,48)",
|
| 3 |
+
"cmds": "#2A7C6E",
|
| 4 |
+
"param": "rgb(247,206,70)",
|
| 5 |
+
"menu": "#427A2E",
|
| 6 |
+
"src": "rgb(216,90,64)",
|
| 7 |
+
"unvl": "grey30",
|
| 8 |
+
"help": "#FAC900"
|
| 9 |
+
}
|
cli/openbb_cli/cli.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Platform CLI entry point."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
from openbb_cli.utils.utils import change_logging_sub_app, reset_logging_sub_app
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def main():
|
| 9 |
+
"""Use the main entry point for the OpenBB Platform CLI."""
|
| 10 |
+
print("Loading...\n") # noqa: T201
|
| 11 |
+
|
| 12 |
+
# pylint: disable=import-outside-toplevel
|
| 13 |
+
from openbb_cli.config.setup import bootstrap
|
| 14 |
+
from openbb_cli.controllers.cli_controller import launch
|
| 15 |
+
|
| 16 |
+
bootstrap()
|
| 17 |
+
|
| 18 |
+
dev = "--dev" in sys.argv[1:]
|
| 19 |
+
debug = "--debug" in sys.argv[1:]
|
| 20 |
+
|
| 21 |
+
launch(dev, debug)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
if __name__ == "__main__":
|
| 25 |
+
initial_logging_sub_app = change_logging_sub_app()
|
| 26 |
+
try:
|
| 27 |
+
main()
|
| 28 |
+
except Exception:
|
| 29 |
+
pass
|
| 30 |
+
finally:
|
| 31 |
+
reset_logging_sub_app(initial_logging_sub_app)
|
cli/openbb_cli/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Core config init."""
|
cli/openbb_cli/config/completer.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Nested completer for completion of OpenBB hierarchical data structures."""
|
| 2 |
+
|
| 3 |
+
from typing import (
|
| 4 |
+
Any,
|
| 5 |
+
Callable,
|
| 6 |
+
Dict,
|
| 7 |
+
Iterable,
|
| 8 |
+
List,
|
| 9 |
+
Mapping,
|
| 10 |
+
Optional,
|
| 11 |
+
Pattern,
|
| 12 |
+
Set,
|
| 13 |
+
Union,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
| 17 |
+
from prompt_toolkit.document import Document
|
| 18 |
+
from prompt_toolkit.formatted_text import AnyFormattedText
|
| 19 |
+
from prompt_toolkit.history import FileHistory
|
| 20 |
+
|
| 21 |
+
NestedDict = Mapping[str, Union[Any, Set[str], None, Completer]]
|
| 22 |
+
|
| 23 |
+
# pylint: disable=too-many-arguments,global-statement,too-many-branches,global-variable-not-assigned
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class WordCompleter(Completer):
|
| 27 |
+
"""Simple autocompletion on a list of words.
|
| 28 |
+
|
| 29 |
+
:param words: List of words or callable that returns a list of words.
|
| 30 |
+
:param ignore_case: If True, case-insensitive completion.
|
| 31 |
+
:param meta_dict: Optional dict mapping words to their meta-text. (This
|
| 32 |
+
should map strings to strings or formatted text.)
|
| 33 |
+
:param WORD: When True, use WORD characters.
|
| 34 |
+
:param sentence: When True, don't complete by comparing the word before the
|
| 35 |
+
cursor, but by comparing all the text before the cursor. In this case,
|
| 36 |
+
the list of words is just a list of strings, where each string can
|
| 37 |
+
contain spaces. (Can not be used together with the WORD option.)
|
| 38 |
+
:param match_middle: When True, match not only the start, but also in the
|
| 39 |
+
middle of the word.
|
| 40 |
+
:param pattern: Optional compiled regex for finding the word before
|
| 41 |
+
the cursor to complete. When given, use this regex pattern instead of
|
| 42 |
+
default one (see document._FIND_WORD_RE)
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(
|
| 46 |
+
self,
|
| 47 |
+
words: Union[List[str], Callable[[], List[str]]],
|
| 48 |
+
ignore_case: bool = False,
|
| 49 |
+
display_dict: Optional[Mapping[str, AnyFormattedText]] = None,
|
| 50 |
+
meta_dict: Optional[Mapping[str, AnyFormattedText]] = None,
|
| 51 |
+
WORD: bool = True,
|
| 52 |
+
sentence: bool = False,
|
| 53 |
+
match_middle: bool = False,
|
| 54 |
+
pattern: Optional[Pattern[str]] = None,
|
| 55 |
+
) -> None:
|
| 56 |
+
"""Initialize the WordCompleter."""
|
| 57 |
+
assert not (WORD and sentence) # noqa: S101
|
| 58 |
+
|
| 59 |
+
self.words = words
|
| 60 |
+
self.ignore_case = ignore_case
|
| 61 |
+
self.display_dict = display_dict or {}
|
| 62 |
+
self.meta_dict = meta_dict or {}
|
| 63 |
+
self.WORD = WORD
|
| 64 |
+
self.sentence = sentence
|
| 65 |
+
self.match_middle = match_middle
|
| 66 |
+
self.pattern = pattern
|
| 67 |
+
|
| 68 |
+
def get_completions(
|
| 69 |
+
self,
|
| 70 |
+
document: Document,
|
| 71 |
+
_complete_event: CompleteEvent,
|
| 72 |
+
) -> Iterable[Completion]:
|
| 73 |
+
"""Get completions."""
|
| 74 |
+
# Get list of words.
|
| 75 |
+
words = self.words
|
| 76 |
+
if callable(words):
|
| 77 |
+
words = words()
|
| 78 |
+
|
| 79 |
+
# Get word/text before cursor.
|
| 80 |
+
if self.sentence:
|
| 81 |
+
word_before_cursor = document.text_before_cursor
|
| 82 |
+
else:
|
| 83 |
+
word_before_cursor = document.get_word_before_cursor(
|
| 84 |
+
WORD=self.WORD, pattern=self.pattern
|
| 85 |
+
)
|
| 86 |
+
if (
|
| 87 |
+
"--" in document.text_before_cursor
|
| 88 |
+
and document.text_before_cursor.rfind(" --")
|
| 89 |
+
>= document.text_before_cursor.rfind(" -")
|
| 90 |
+
):
|
| 91 |
+
word_before_cursor = f'--{document.text_before_cursor.split("--")[-1]}'
|
| 92 |
+
elif f"--{word_before_cursor}" == document.text_before_cursor:
|
| 93 |
+
word_before_cursor = document.text_before_cursor
|
| 94 |
+
|
| 95 |
+
if self.ignore_case:
|
| 96 |
+
word_before_cursor = word_before_cursor.lower()
|
| 97 |
+
|
| 98 |
+
def word_matches(word: str) -> bool:
|
| 99 |
+
"""Set True when the word before the cursor matches."""
|
| 100 |
+
if self.ignore_case:
|
| 101 |
+
word = word.lower()
|
| 102 |
+
|
| 103 |
+
if self.match_middle:
|
| 104 |
+
return word_before_cursor in word
|
| 105 |
+
return word.startswith(word_before_cursor)
|
| 106 |
+
|
| 107 |
+
for a in words:
|
| 108 |
+
if word_matches(a):
|
| 109 |
+
display = self.display_dict.get(a, a)
|
| 110 |
+
display_meta = self.meta_dict.get(a, "")
|
| 111 |
+
yield Completion(
|
| 112 |
+
text=a,
|
| 113 |
+
start_position=-len(word_before_cursor),
|
| 114 |
+
display=display,
|
| 115 |
+
display_meta=display_meta,
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class NestedCompleter(Completer):
|
| 120 |
+
"""Completer which wraps around several other completers, and calls any the
|
| 121 |
+
one that corresponds with the first word of the input.
|
| 122 |
+
|
| 123 |
+
By combining multiple `NestedCompleter` instances, we can achieve multiple
|
| 124 |
+
hierarchical levels of autocompletion. This is useful when `WordCompleter`
|
| 125 |
+
is not sufficient.
|
| 126 |
+
|
| 127 |
+
If you need multiple levels, check out the `from_nested_dict` classmethod.
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
complementary: List = list()
|
| 131 |
+
|
| 132 |
+
def __init__(
|
| 133 |
+
self, options: Dict[str, Optional[Completer]], ignore_case: bool = True
|
| 134 |
+
) -> None:
|
| 135 |
+
"""Initialize the NestedCompleter."""
|
| 136 |
+
self.flags_processed: List = list()
|
| 137 |
+
self.original_options = options
|
| 138 |
+
self.options = options
|
| 139 |
+
self.ignore_case = ignore_case
|
| 140 |
+
self.complementary = list()
|
| 141 |
+
|
| 142 |
+
def __repr__(self) -> str:
|
| 143 |
+
"""Return string representation of NestedCompleter."""
|
| 144 |
+
return f"NestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
|
| 145 |
+
|
| 146 |
+
@classmethod
|
| 147 |
+
def from_nested_dict(cls, data: dict) -> "NestedCompleter":
|
| 148 |
+
"""Create a `NestedCompleter`.
|
| 149 |
+
|
| 150 |
+
It starts from a nested dictionary data structure, like this:
|
| 151 |
+
|
| 152 |
+
.. code::
|
| 153 |
+
|
| 154 |
+
data = {
|
| 155 |
+
'show': {
|
| 156 |
+
'version': None,
|
| 157 |
+
'interfaces': None,
|
| 158 |
+
'clock': None,
|
| 159 |
+
'ip': {'interface': {'brief'}}
|
| 160 |
+
},
|
| 161 |
+
'exit': None
|
| 162 |
+
'enable': None
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
The value should be `None` if there is no further completion at some
|
| 166 |
+
point. If all values in the dictionary are None, it is also possible to
|
| 167 |
+
use a set instead.
|
| 168 |
+
|
| 169 |
+
Values in this data structure can be a completers as well.
|
| 170 |
+
"""
|
| 171 |
+
options: Dict[str, Any] = {}
|
| 172 |
+
for key, value in data.items():
|
| 173 |
+
if isinstance(value, Completer):
|
| 174 |
+
options[key] = value
|
| 175 |
+
elif isinstance(value, dict):
|
| 176 |
+
options[key] = cls.from_nested_dict(value)
|
| 177 |
+
elif isinstance(value, set):
|
| 178 |
+
options[key] = cls.from_nested_dict({item: None for item in value})
|
| 179 |
+
elif isinstance(key, str) and isinstance(value, str):
|
| 180 |
+
options[key] = options[value]
|
| 181 |
+
else:
|
| 182 |
+
assert value is None # noqa: S101
|
| 183 |
+
options[key] = None
|
| 184 |
+
|
| 185 |
+
for items in cls.complementary:
|
| 186 |
+
if items[0] in options:
|
| 187 |
+
options[items[1]] = options[items[0]]
|
| 188 |
+
elif items[1] in options:
|
| 189 |
+
options[items[0]] = options[items[1]]
|
| 190 |
+
|
| 191 |
+
return cls(options)
|
| 192 |
+
|
| 193 |
+
def get_completions( # noqa: PLR0912
|
| 194 |
+
self, document: Document, complete_event: CompleteEvent
|
| 195 |
+
) -> Iterable[Completion]:
|
| 196 |
+
"""Get completions."""
|
| 197 |
+
# Split document.
|
| 198 |
+
cmd = ""
|
| 199 |
+
text = document.text_before_cursor.lstrip()
|
| 200 |
+
if " " in text:
|
| 201 |
+
cmd = text.split(" ")[0]
|
| 202 |
+
if "-" in text:
|
| 203 |
+
if text.rfind("--") == -1 or text.rfind("-") - 1 > text.rfind("--"):
|
| 204 |
+
unprocessed_text = "-" + text.split("-")[-1]
|
| 205 |
+
else:
|
| 206 |
+
unprocessed_text = "--" + text.split("--")[-1]
|
| 207 |
+
else:
|
| 208 |
+
unprocessed_text = text
|
| 209 |
+
stripped_len = len(document.text_before_cursor) - len(text)
|
| 210 |
+
|
| 211 |
+
# Check if there are multiple flags for the same command
|
| 212 |
+
if self.complementary:
|
| 213 |
+
for same_flags in self.complementary:
|
| 214 |
+
if (
|
| 215 |
+
same_flags[0] in self.flags_processed
|
| 216 |
+
and same_flags[1] not in self.flags_processed
|
| 217 |
+
) or (
|
| 218 |
+
same_flags[1] in self.flags_processed
|
| 219 |
+
and same_flags[0] not in self.flags_processed
|
| 220 |
+
):
|
| 221 |
+
if same_flags[0] in self.flags_processed:
|
| 222 |
+
self.flags_processed.append(same_flags[1])
|
| 223 |
+
elif same_flags[1] in self.flags_processed:
|
| 224 |
+
self.flags_processed.append(same_flags[0])
|
| 225 |
+
|
| 226 |
+
if cmd:
|
| 227 |
+
self.options = {
|
| 228 |
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
| 229 |
+
for k in self.original_options.get(cmd).options # type: ignore
|
| 230 |
+
if k not in self.flags_processed
|
| 231 |
+
}
|
| 232 |
+
else:
|
| 233 |
+
self.options = {
|
| 234 |
+
k: self.original_options[k]
|
| 235 |
+
for k in self.original_options
|
| 236 |
+
if k not in self.flags_processed
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
# If there is a space, check for the first term, and use a subcompleter.
|
| 240 |
+
if " " in unprocessed_text:
|
| 241 |
+
first_term = unprocessed_text.split()[0]
|
| 242 |
+
|
| 243 |
+
# user is updating one of the values
|
| 244 |
+
if unprocessed_text[-1] != " ":
|
| 245 |
+
self.flags_processed = [
|
| 246 |
+
flag for flag in self.flags_processed if flag != first_term
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
if self.complementary:
|
| 250 |
+
for same_flags in self.complementary:
|
| 251 |
+
if (
|
| 252 |
+
same_flags[0] in self.flags_processed
|
| 253 |
+
and same_flags[1] not in self.flags_processed
|
| 254 |
+
) or (
|
| 255 |
+
same_flags[1] in self.flags_processed
|
| 256 |
+
and same_flags[0] not in self.flags_processed
|
| 257 |
+
):
|
| 258 |
+
if same_flags[0] in self.flags_processed:
|
| 259 |
+
self.flags_processed.remove(same_flags[0])
|
| 260 |
+
elif same_flags[1] in self.flags_processed:
|
| 261 |
+
self.flags_processed.remove(same_flags[1])
|
| 262 |
+
|
| 263 |
+
if cmd and self.original_options.get(cmd):
|
| 264 |
+
self.options = self.original_options
|
| 265 |
+
else:
|
| 266 |
+
self.options = {
|
| 267 |
+
k: self.original_options[k]
|
| 268 |
+
for k in self.original_options
|
| 269 |
+
if k not in self.flags_processed
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
if "-" not in text:
|
| 273 |
+
completer = self.options.get(first_term)
|
| 274 |
+
elif cmd in self.options and self.options.get(cmd):
|
| 275 |
+
completer = self.options.get(cmd).options.get(first_term) # type: ignore
|
| 276 |
+
else:
|
| 277 |
+
completer = self.options.get(first_term)
|
| 278 |
+
|
| 279 |
+
# If we have a sub completer, use this for the completions.
|
| 280 |
+
if completer is not None:
|
| 281 |
+
remaining_text = unprocessed_text[len(first_term) :].lstrip()
|
| 282 |
+
move_cursor = len(text) - len(remaining_text) + stripped_len
|
| 283 |
+
|
| 284 |
+
new_document = Document(
|
| 285 |
+
remaining_text,
|
| 286 |
+
cursor_position=document.cursor_position - move_cursor,
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# Provides auto-completion but if user doesn't take it still keep going
|
| 290 |
+
if " " in new_document.text:
|
| 291 |
+
if (
|
| 292 |
+
new_document.text in [f"{opt} " for opt in self.options]
|
| 293 |
+
or unprocessed_text[-1] == " "
|
| 294 |
+
):
|
| 295 |
+
self.flags_processed.append(first_term)
|
| 296 |
+
if cmd:
|
| 297 |
+
self.options = {
|
| 298 |
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
| 299 |
+
for k in self.original_options.get(cmd).options # type: ignore
|
| 300 |
+
if k not in self.flags_processed
|
| 301 |
+
}
|
| 302 |
+
else:
|
| 303 |
+
self.options = {
|
| 304 |
+
k: self.original_options[k]
|
| 305 |
+
for k in self.original_options
|
| 306 |
+
if k not in self.flags_processed
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
# In case the users inputs a single boolean flag
|
| 310 |
+
elif not completer.options: # type: ignore
|
| 311 |
+
self.flags_processed.append(first_term)
|
| 312 |
+
|
| 313 |
+
if self.complementary:
|
| 314 |
+
for same_flags in self.complementary:
|
| 315 |
+
if (
|
| 316 |
+
same_flags[0] in self.flags_processed
|
| 317 |
+
and same_flags[1] not in self.flags_processed
|
| 318 |
+
) or (
|
| 319 |
+
same_flags[1] in self.flags_processed
|
| 320 |
+
and same_flags[0] not in self.flags_processed
|
| 321 |
+
):
|
| 322 |
+
if same_flags[0] in self.flags_processed:
|
| 323 |
+
self.flags_processed.append(same_flags[1])
|
| 324 |
+
elif same_flags[1] in self.flags_processed:
|
| 325 |
+
self.flags_processed.append(same_flags[0])
|
| 326 |
+
|
| 327 |
+
if cmd:
|
| 328 |
+
self.options = {
|
| 329 |
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
| 330 |
+
for k in self.original_options.get(cmd).options # type: ignore
|
| 331 |
+
if k not in self.flags_processed
|
| 332 |
+
}
|
| 333 |
+
else:
|
| 334 |
+
self.options = {
|
| 335 |
+
k: self.original_options[k]
|
| 336 |
+
for k in self.original_options
|
| 337 |
+
if k not in self.flags_processed
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
else:
|
| 341 |
+
# This is a NestedCompleter
|
| 342 |
+
yield from completer.get_completions(new_document, complete_event)
|
| 343 |
+
|
| 344 |
+
# No space in the input: behave exactly like `WordCompleter`.
|
| 345 |
+
else:
|
| 346 |
+
# check if the prompt has been updated in the meantime
|
| 347 |
+
if " " in text or "-" in text:
|
| 348 |
+
actual_flags_processed = [
|
| 349 |
+
flag for flag in self.flags_processed if flag in text
|
| 350 |
+
]
|
| 351 |
+
|
| 352 |
+
if self.complementary:
|
| 353 |
+
for same_flags in self.complementary:
|
| 354 |
+
if (
|
| 355 |
+
same_flags[0] in actual_flags_processed
|
| 356 |
+
and same_flags[1] not in actual_flags_processed
|
| 357 |
+
) or (
|
| 358 |
+
same_flags[1] in actual_flags_processed
|
| 359 |
+
and same_flags[0] not in actual_flags_processed
|
| 360 |
+
):
|
| 361 |
+
if same_flags[0] in actual_flags_processed:
|
| 362 |
+
actual_flags_processed.append(same_flags[1])
|
| 363 |
+
elif same_flags[1] in actual_flags_processed:
|
| 364 |
+
actual_flags_processed.append(same_flags[0])
|
| 365 |
+
|
| 366 |
+
if len(actual_flags_processed) < len(self.flags_processed):
|
| 367 |
+
self.flags_processed = actual_flags_processed
|
| 368 |
+
if cmd:
|
| 369 |
+
self.options = {
|
| 370 |
+
k: self.original_options.get(cmd).options[k] # type: ignore
|
| 371 |
+
for k in self.original_options.get(cmd).options # type: ignore
|
| 372 |
+
if k not in self.flags_processed
|
| 373 |
+
}
|
| 374 |
+
else:
|
| 375 |
+
self.options = {
|
| 376 |
+
k: self.original_options[k]
|
| 377 |
+
for k in self.original_options
|
| 378 |
+
if k not in self.flags_processed
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
command = self.options.get(cmd)
|
| 382 |
+
options = command.options if command else {} # type: ignore
|
| 383 |
+
command_options = [f"{cmd} {opt}" for opt in options]
|
| 384 |
+
text_list = [text in val for val in command_options]
|
| 385 |
+
if cmd and cmd in self.options and text_list:
|
| 386 |
+
completer = WordCompleter(
|
| 387 |
+
list(self.options.get(cmd).options.keys()), # type: ignore
|
| 388 |
+
ignore_case=self.ignore_case,
|
| 389 |
+
)
|
| 390 |
+
elif bool([val for val in self.options if text in val]):
|
| 391 |
+
completer = WordCompleter(
|
| 392 |
+
list(self.options.keys()), ignore_case=self.ignore_case
|
| 393 |
+
)
|
| 394 |
+
else:
|
| 395 |
+
# The user has delete part of the first command and we need to reset options
|
| 396 |
+
if bool([val for val in self.original_options if text in val]):
|
| 397 |
+
self.options = self.original_options
|
| 398 |
+
self.flags_processed = list()
|
| 399 |
+
completer = WordCompleter(
|
| 400 |
+
list(self.options.keys()), ignore_case=self.ignore_case
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
# This is a WordCompleter
|
| 404 |
+
yield from completer.get_completions(document, complete_event)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
class CustomFileHistory(FileHistory):
|
| 408 |
+
"""Filtered file history."""
|
| 409 |
+
|
| 410 |
+
def sanitize_input(self, string: str) -> str:
|
| 411 |
+
"""Sanitize sensitive information from the input string by parsing arguments."""
|
| 412 |
+
keywords = ["--password", "--email", "--pat"]
|
| 413 |
+
string_list = string.split(" ")
|
| 414 |
+
|
| 415 |
+
for kw in keywords:
|
| 416 |
+
if kw in string_list:
|
| 417 |
+
index = string_list.index(kw)
|
| 418 |
+
if len(string_list) > index + 1:
|
| 419 |
+
string_list[index + 1] = "********"
|
| 420 |
+
|
| 421 |
+
result = " ".join(string_list)
|
| 422 |
+
return result
|
| 423 |
+
|
| 424 |
+
def store_string(self, string: str) -> None:
|
| 425 |
+
"""Store string in history."""
|
| 426 |
+
string = self.sanitize_input(string)
|
| 427 |
+
super().store_string(string)
|
cli/openbb_cli/config/console.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
| 2 |
+
|
| 3 |
+
from rich import panel
|
| 4 |
+
from rich.console import (
|
| 5 |
+
Console as RichConsole,
|
| 6 |
+
Theme,
|
| 7 |
+
)
|
| 8 |
+
from rich.text import Text
|
| 9 |
+
|
| 10 |
+
from openbb_cli.config.menu_text import RICH_TAGS
|
| 11 |
+
|
| 12 |
+
if TYPE_CHECKING:
|
| 13 |
+
from openbb_cli.models.settings import Settings
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Console:
|
| 17 |
+
"""Create a rich console to wrap the console print with a Panel."""
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
settings: "Settings",
|
| 22 |
+
style: Optional[Dict[str, Any]] = None,
|
| 23 |
+
):
|
| 24 |
+
"""Initialize the ConsoleAndPanel class."""
|
| 25 |
+
self._console = RichConsole(
|
| 26 |
+
theme=Theme(style),
|
| 27 |
+
highlight=False,
|
| 28 |
+
soft_wrap=True,
|
| 29 |
+
)
|
| 30 |
+
self._settings = settings
|
| 31 |
+
self.menu_text = ""
|
| 32 |
+
self.menu_path = ""
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
def _filter_rich_tags(text):
|
| 36 |
+
"""Filter out rich tags from text."""
|
| 37 |
+
for val in RICH_TAGS:
|
| 38 |
+
text = text.replace(val, "")
|
| 39 |
+
|
| 40 |
+
return text
|
| 41 |
+
|
| 42 |
+
@staticmethod
|
| 43 |
+
def _blend_text(
|
| 44 |
+
message: str, color1: Tuple[int, int, int], color2: Tuple[int, int, int]
|
| 45 |
+
) -> Text:
|
| 46 |
+
"""Blend text from one color to another."""
|
| 47 |
+
text = Text(message)
|
| 48 |
+
r1, g1, b1 = color1
|
| 49 |
+
r2, g2, b2 = color2
|
| 50 |
+
dr = r2 - r1
|
| 51 |
+
dg = g2 - g1
|
| 52 |
+
db = b2 - b1
|
| 53 |
+
size = len(text) + 5
|
| 54 |
+
for index in range(size):
|
| 55 |
+
blend = index / size
|
| 56 |
+
color = f"#{int(r1 + dr * blend):02X}{int(g1 + dg * blend):02X}{int(b1 + db * blend):02X}"
|
| 57 |
+
text.stylize(color, index, index + 1)
|
| 58 |
+
return text
|
| 59 |
+
|
| 60 |
+
def print(self, *args, **kwargs):
|
| 61 |
+
"""Print the text to the console."""
|
| 62 |
+
if kwargs and "text" in list(kwargs) and "menu" in list(kwargs):
|
| 63 |
+
if not self._settings.TEST_MODE:
|
| 64 |
+
if self._settings.ENABLE_RICH_PANEL:
|
| 65 |
+
if self._settings.SHOW_VERSION:
|
| 66 |
+
version = self._settings.VERSION
|
| 67 |
+
version = f"[param]OpenBB Platform CLI v{version}[/param] (https://openbb.co)"
|
| 68 |
+
else:
|
| 69 |
+
version = (
|
| 70 |
+
"[param]OpenBB Platform CLI[/param] (https://openbb.co)"
|
| 71 |
+
)
|
| 72 |
+
self._console.print(
|
| 73 |
+
panel.Panel(
|
| 74 |
+
"\n" + kwargs["text"],
|
| 75 |
+
title=kwargs["menu"],
|
| 76 |
+
subtitle_align="right",
|
| 77 |
+
subtitle=version,
|
| 78 |
+
)
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
else:
|
| 82 |
+
self._console.print(kwargs["text"])
|
| 83 |
+
else:
|
| 84 |
+
print(self._filter_rich_tags(kwargs["text"])) # noqa: T201
|
| 85 |
+
elif not self._settings.TEST_MODE:
|
| 86 |
+
self._console.print(*args, **kwargs)
|
| 87 |
+
else:
|
| 88 |
+
print(*args, **kwargs) # noqa: T201
|
| 89 |
+
|
| 90 |
+
def input(self, *args, **kwargs):
|
| 91 |
+
"""Get input from the user."""
|
| 92 |
+
self.print(*args, **kwargs, end="")
|
| 93 |
+
return input()
|
cli/openbb_cli/config/constants.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Constants module."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
# Paths
|
| 6 |
+
HOME_DIRECTORY = Path.home()
|
| 7 |
+
REPOSITORY_DIRECTORY = Path(__file__).parent.parent.parent.parent
|
| 8 |
+
SRC_DIRECTORY = Path(__file__).parent.parent
|
| 9 |
+
SETTINGS_DIRECTORY = HOME_DIRECTORY / ".openbb_platform"
|
| 10 |
+
ASSETS_DIRECTORY = SRC_DIRECTORY / "assets"
|
| 11 |
+
STYLES_DIRECTORY = ASSETS_DIRECTORY / "styles"
|
| 12 |
+
ENV_FILE_SETTINGS = SETTINGS_DIRECTORY / ".cli.env"
|
| 13 |
+
HIST_FILE_PROMPT = SETTINGS_DIRECTORY / ".cli.his"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
DEFAULT_ROUTINES_URL = "https://openbb-cms.directus.app/items/Routines"
|
| 17 |
+
TIMEOUT = 30
|
| 18 |
+
CONNECTION_ERROR_MSG = "[red]Connection error.[/red]"
|
| 19 |
+
CONNECTION_TIMEOUT_MSG = "[red]Connection timeout.[/red]"
|
| 20 |
+
SCRIPT_TAGS = [
|
| 21 |
+
"stocks",
|
| 22 |
+
"crypto",
|
| 23 |
+
"etf",
|
| 24 |
+
"economy",
|
| 25 |
+
"forex",
|
| 26 |
+
"fixed income",
|
| 27 |
+
"alternative",
|
| 28 |
+
"funds",
|
| 29 |
+
"bonds",
|
| 30 |
+
"macro",
|
| 31 |
+
"mutual funds",
|
| 32 |
+
"equities",
|
| 33 |
+
"options",
|
| 34 |
+
"dark pool",
|
| 35 |
+
"shorts",
|
| 36 |
+
"insider",
|
| 37 |
+
"behavioral analysis",
|
| 38 |
+
"fundamental analysis",
|
| 39 |
+
"technical analysis",
|
| 40 |
+
"quantitative analysis",
|
| 41 |
+
"forecasting",
|
| 42 |
+
"government",
|
| 43 |
+
"comparison",
|
| 44 |
+
"nft",
|
| 45 |
+
"on chain",
|
| 46 |
+
"off chain",
|
| 47 |
+
"screener",
|
| 48 |
+
"report",
|
| 49 |
+
"overview",
|
| 50 |
+
"rates",
|
| 51 |
+
"econometrics",
|
| 52 |
+
"portfolio",
|
| 53 |
+
"real estate",
|
| 54 |
+
]
|
| 55 |
+
AVAILABLE_FLAIRS = {
|
| 56 |
+
":openbb": "(🦋)",
|
| 57 |
+
":bug": "(🐛)",
|
| 58 |
+
":rocket": "(🚀)",
|
| 59 |
+
":diamond": "(💎)",
|
| 60 |
+
":stars": "(✨)",
|
| 61 |
+
":baseball": "(⚾)",
|
| 62 |
+
":boat": "(⛵)",
|
| 63 |
+
":phone": "(☎)",
|
| 64 |
+
":mercury": "(☿)",
|
| 65 |
+
":hidden": "",
|
| 66 |
+
":sun": "(☼)",
|
| 67 |
+
":moon": "(🌕)",
|
| 68 |
+
":nuke": "(☢)",
|
| 69 |
+
":hazard": "(☣)",
|
| 70 |
+
":tunder": "(☈)",
|
| 71 |
+
":king": "(♔)",
|
| 72 |
+
":queen": "(♕)",
|
| 73 |
+
":knight": "(♘)",
|
| 74 |
+
":recycle": "(♻)",
|
| 75 |
+
":scales": "(⚖)",
|
| 76 |
+
":ball": "(⚽)",
|
| 77 |
+
":golf": "(⛳)",
|
| 78 |
+
":peace": "(☮)",
|
| 79 |
+
":yy": "(☯)",
|
| 80 |
+
}
|
cli/openbb_cli/config/menu_text.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rich Module."""
|
| 2 |
+
|
| 3 |
+
__docformat__ = "numpy"
|
| 4 |
+
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
from openbb import obb
|
| 8 |
+
|
| 9 |
+
# https://rich.readthedocs.io/en/stable/appendix/colors.html#appendix-colors
|
| 10 |
+
# https://rich.readthedocs.io/en/latest/highlighting.html#custom-highlighters
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
RICH_TAGS = [
|
| 14 |
+
"[menu]",
|
| 15 |
+
"[/menu]",
|
| 16 |
+
"[cmds]",
|
| 17 |
+
"[/cmds]",
|
| 18 |
+
"[info]",
|
| 19 |
+
"[/info]",
|
| 20 |
+
"[param]",
|
| 21 |
+
"[/param]",
|
| 22 |
+
"[src]",
|
| 23 |
+
"[/src]",
|
| 24 |
+
"[help]",
|
| 25 |
+
"[/help]",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class MenuText:
|
| 30 |
+
"""Create menu text with rich colors to be displayed by CLI."""
|
| 31 |
+
|
| 32 |
+
CMD_NAME_LENGTH = 23
|
| 33 |
+
CMD_DESCRIPTION_LENGTH = 65
|
| 34 |
+
CMD_PROVIDERS_LENGTH = 23
|
| 35 |
+
SECTION_SPACING = 4
|
| 36 |
+
|
| 37 |
+
def __init__(self, path: str = ""):
|
| 38 |
+
"""Initialize menu help."""
|
| 39 |
+
self.menu_text = ""
|
| 40 |
+
self.menu_path = path
|
| 41 |
+
self.warnings: List[Dict[str, str]] = []
|
| 42 |
+
|
| 43 |
+
@staticmethod
|
| 44 |
+
def _get_providers(command_path: str) -> List:
|
| 45 |
+
"""Return the preferred provider for the given command.
|
| 46 |
+
|
| 47 |
+
Parameters
|
| 48 |
+
----------
|
| 49 |
+
command_path: str
|
| 50 |
+
The command to find the provider for. E.g. "/equity/price/historical
|
| 51 |
+
|
| 52 |
+
Returns
|
| 53 |
+
-------
|
| 54 |
+
List
|
| 55 |
+
The list of providers for the given command.
|
| 56 |
+
"""
|
| 57 |
+
command_reference = obb.reference.get("paths", {}).get(command_path, {}) # type: ignore
|
| 58 |
+
if command_reference:
|
| 59 |
+
providers = list(command_reference["parameters"].keys())
|
| 60 |
+
return [provider for provider in providers if provider != "standard"]
|
| 61 |
+
return []
|
| 62 |
+
|
| 63 |
+
def _format_cmd_name(self, name: str) -> str:
|
| 64 |
+
"""Truncate command name length if it is too long."""
|
| 65 |
+
if len(name) > self.CMD_NAME_LENGTH:
|
| 66 |
+
new_name = name[: self.CMD_NAME_LENGTH]
|
| 67 |
+
|
| 68 |
+
if "_" in name:
|
| 69 |
+
name_split = name.split("_")
|
| 70 |
+
|
| 71 |
+
new_name = (
|
| 72 |
+
"_".join(name_split[:2]) if len(name_split) > 2 else name_split[0]
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
if len(new_name) > self.CMD_NAME_LENGTH:
|
| 76 |
+
new_name = new_name[: self.CMD_NAME_LENGTH]
|
| 77 |
+
|
| 78 |
+
if new_name != name:
|
| 79 |
+
self.warnings.append(
|
| 80 |
+
{
|
| 81 |
+
"warning": "Command name too long",
|
| 82 |
+
"actual command": f"`{name}`",
|
| 83 |
+
"displayed command": f"`{new_name}`",
|
| 84 |
+
}
|
| 85 |
+
)
|
| 86 |
+
name = new_name
|
| 87 |
+
|
| 88 |
+
return name
|
| 89 |
+
|
| 90 |
+
def _format_cmd_description(
|
| 91 |
+
self, name: str, description: str, trim: bool = True
|
| 92 |
+
) -> str:
|
| 93 |
+
"""Truncate command description length if it is too long."""
|
| 94 |
+
if not description or description == f"{self.menu_path}{name}":
|
| 95 |
+
description = ""
|
| 96 |
+
return (
|
| 97 |
+
description[: self.CMD_DESCRIPTION_LENGTH - 3] + "..."
|
| 98 |
+
if len(description) > self.CMD_DESCRIPTION_LENGTH and trim
|
| 99 |
+
else description
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
def add_raw(self, text: str, left_spacing: bool = False):
|
| 103 |
+
"""Append raw text (without translation)."""
|
| 104 |
+
if left_spacing:
|
| 105 |
+
self.menu_text += f"{self.SECTION_SPACING * ' '}{text}\n"
|
| 106 |
+
else:
|
| 107 |
+
self.menu_text += text
|
| 108 |
+
|
| 109 |
+
def add_info(self, text: str):
|
| 110 |
+
"""Append information text (after translation)."""
|
| 111 |
+
self.menu_text += f"[info]{text}:[/info]\n"
|
| 112 |
+
|
| 113 |
+
def add_cmd(self, name: str, description: str = "", disable: bool = False):
|
| 114 |
+
"""Append command text (after translation)."""
|
| 115 |
+
formatted_name = self._format_cmd_name(name)
|
| 116 |
+
name_padding = (self.CMD_NAME_LENGTH - len(formatted_name)) * " "
|
| 117 |
+
providers = self._get_providers(f"{self.menu_path}{name}")
|
| 118 |
+
formatted_description = self._format_cmd_description(
|
| 119 |
+
formatted_name,
|
| 120 |
+
description,
|
| 121 |
+
bool(providers),
|
| 122 |
+
)
|
| 123 |
+
description_padding = (
|
| 124 |
+
self.CMD_DESCRIPTION_LENGTH - len(formatted_description)
|
| 125 |
+
) * " "
|
| 126 |
+
spacing = self.SECTION_SPACING * " "
|
| 127 |
+
description_padding = (
|
| 128 |
+
self.CMD_DESCRIPTION_LENGTH - len(formatted_description)
|
| 129 |
+
) * " "
|
| 130 |
+
cmd = f"{spacing}{formatted_name + name_padding}{spacing}{formatted_description+description_padding}"
|
| 131 |
+
cmd = f"[unvl]{cmd}[/unvl]" if disable else f"[cmds]{cmd}[/cmds]"
|
| 132 |
+
|
| 133 |
+
if providers:
|
| 134 |
+
cmd += rf"{spacing}[src]\[{', '.join(providers)}][/src]"
|
| 135 |
+
|
| 136 |
+
self.menu_text += cmd + "\n"
|
| 137 |
+
|
| 138 |
+
def add_menu(
|
| 139 |
+
self,
|
| 140 |
+
name: str,
|
| 141 |
+
description: str = "",
|
| 142 |
+
disable: bool = False,
|
| 143 |
+
):
|
| 144 |
+
"""Append menu text (after translation)."""
|
| 145 |
+
spacing = (self.CMD_NAME_LENGTH - len(name) + self.SECTION_SPACING) * " "
|
| 146 |
+
|
| 147 |
+
if not description or description == f"{self.menu_path}{name}":
|
| 148 |
+
description = ""
|
| 149 |
+
|
| 150 |
+
if len(description) > self.CMD_DESCRIPTION_LENGTH:
|
| 151 |
+
description = description[: self.CMD_DESCRIPTION_LENGTH - 3] + "..."
|
| 152 |
+
|
| 153 |
+
menu = f"{name}{spacing}{description}"
|
| 154 |
+
tag = "unvl" if disable else "menu"
|
| 155 |
+
self.menu_text += f"[{tag}]> {menu}[/{tag}]\n"
|
| 156 |
+
|
| 157 |
+
def add_setting(self, name: str, status: bool = True, description: str = ""):
|
| 158 |
+
"""Append menu text (after translation)."""
|
| 159 |
+
spacing = (self.CMD_NAME_LENGTH - len(name) + self.SECTION_SPACING) * " "
|
| 160 |
+
indentation = self.SECTION_SPACING * " "
|
| 161 |
+
color = "green" if status else "red"
|
| 162 |
+
|
| 163 |
+
self.menu_text += (
|
| 164 |
+
f"[{color}]{indentation}{name}{spacing}{description}[/{color}]\n"
|
| 165 |
+
)
|
cli/openbb_cli/config/setup.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for the CLI."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from openbb_cli.config.constants import ENV_FILE_SETTINGS, SETTINGS_DIRECTORY
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def bootstrap():
|
| 9 |
+
"""Setup pre-launch configurations for the CLI."""
|
| 10 |
+
SETTINGS_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
| 11 |
+
Path(ENV_FILE_SETTINGS).touch(exist_ok=True)
|
cli/openbb_cli/config/style.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chart and style helpers for Plotly."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=C0302,R0902,W3301
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
from rich.console import Console
|
| 9 |
+
|
| 10 |
+
from openbb_cli.config.constants import STYLES_DIRECTORY
|
| 11 |
+
|
| 12 |
+
console = Console()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class Style:
|
| 16 |
+
"""The class that helps with handling of style configurations.
|
| 17 |
+
|
| 18 |
+
It serves styles for 2 libraries. For `Plotly` this class serves absolute paths
|
| 19 |
+
to the .pltstyle files. For `Plotly` and `Rich` this class serves custom
|
| 20 |
+
styles as python dictionaries.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
STYLES_REPO = STYLES_DIRECTORY
|
| 24 |
+
|
| 25 |
+
console_styles_available: Dict[str, Path] = {}
|
| 26 |
+
console_style: Dict[str, Any] = {}
|
| 27 |
+
|
| 28 |
+
line_color: str = ""
|
| 29 |
+
up_color: str = ""
|
| 30 |
+
down_color: str = ""
|
| 31 |
+
up_colorway: List[str] = []
|
| 32 |
+
down_colorway: List[str] = []
|
| 33 |
+
up_color_transparent: str = ""
|
| 34 |
+
down_color_transparent: str = ""
|
| 35 |
+
|
| 36 |
+
line_width: float = 1.5
|
| 37 |
+
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
style: Optional[str] = "",
|
| 41 |
+
directory: Optional[Path] = None,
|
| 42 |
+
):
|
| 43 |
+
"""Initialize the class."""
|
| 44 |
+
self._load(directory)
|
| 45 |
+
self.apply(style, directory)
|
| 46 |
+
|
| 47 |
+
def apply(
|
| 48 |
+
self, style: Optional[str] = None, directory: Optional[Path] = None
|
| 49 |
+
) -> None:
|
| 50 |
+
"""Apply the style to the console."""
|
| 51 |
+
if style:
|
| 52 |
+
if style in self.console_styles_available:
|
| 53 |
+
json_path: Optional[Path] = self.console_styles_available[style]
|
| 54 |
+
else:
|
| 55 |
+
self._load(directory)
|
| 56 |
+
if style in self.console_styles_available:
|
| 57 |
+
json_path = self.console_styles_available[style]
|
| 58 |
+
else:
|
| 59 |
+
console.print(f"\nInvalid console style '{style}', using default.")
|
| 60 |
+
json_path = self.console_styles_available.get("dark", None)
|
| 61 |
+
|
| 62 |
+
if json_path:
|
| 63 |
+
self.console_style = self._from_json(json_path)
|
| 64 |
+
else:
|
| 65 |
+
console.print("Error loading default.")
|
| 66 |
+
|
| 67 |
+
def _from_directory(self, folder: Optional[Path]) -> None:
|
| 68 |
+
"""Load custom styles from folder.
|
| 69 |
+
|
| 70 |
+
Parses the styles/default and styles/user folders and loads style files.
|
| 71 |
+
To be recognized files need to follow a naming convention:
|
| 72 |
+
*.pltstyle - plotly stylesheets
|
| 73 |
+
*.richstyle.json - rich stylesheets
|
| 74 |
+
|
| 75 |
+
Parameters
|
| 76 |
+
----------
|
| 77 |
+
folder : str
|
| 78 |
+
Path to the folder containing the stylesheets
|
| 79 |
+
"""
|
| 80 |
+
if not folder or not folder.exists():
|
| 81 |
+
return
|
| 82 |
+
|
| 83 |
+
for attr, ext in zip(
|
| 84 |
+
["console_styles_available"],
|
| 85 |
+
[".richstyle.json"],
|
| 86 |
+
):
|
| 87 |
+
for file in folder.rglob(f"*{ext}"):
|
| 88 |
+
getattr(self, attr)[file.name.replace(ext, "")] = file
|
| 89 |
+
|
| 90 |
+
def _load(self, directory: Optional[Path] = None) -> None:
|
| 91 |
+
"""Load custom styles from default and user folders."""
|
| 92 |
+
self._from_directory(self.STYLES_REPO)
|
| 93 |
+
self._from_directory(directory)
|
| 94 |
+
|
| 95 |
+
def _from_json(self, file: Path) -> Dict[str, Any]:
|
| 96 |
+
"""Load style from json file."""
|
| 97 |
+
with open(file) as f:
|
| 98 |
+
json_style: dict = json.load(f)
|
| 99 |
+
for key, value in json_style.items():
|
| 100 |
+
json_style[key] = value.replace(
|
| 101 |
+
" ", ""
|
| 102 |
+
) # remove whitespaces so Rich can parse it
|
| 103 |
+
return json_style
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
def available_styles(self) -> List[str]:
|
| 107 |
+
"""Return available styles."""
|
| 108 |
+
return list(self.console_styles_available.keys())
|
cli/openbb_cli/controllers/base_controller.py
ADDED
|
@@ -0,0 +1,1032 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Base controller for the CLI."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import difflib
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import shlex
|
| 8 |
+
from abc import ABCMeta, abstractmethod
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
| 12 |
+
|
| 13 |
+
import pandas as pd
|
| 14 |
+
from openbb_cli.config.completer import NestedCompleter
|
| 15 |
+
from openbb_cli.config.constants import SCRIPT_TAGS
|
| 16 |
+
from openbb_cli.controllers.choices import build_controller_choice_map
|
| 17 |
+
from openbb_cli.controllers.hub_service import upload_routine
|
| 18 |
+
from openbb_cli.controllers.utils import (
|
| 19 |
+
check_file_type_saved,
|
| 20 |
+
check_positive,
|
| 21 |
+
get_flair_and_username,
|
| 22 |
+
handle_obbject_display,
|
| 23 |
+
parse_and_split_input,
|
| 24 |
+
parse_unknown_args_to_dict,
|
| 25 |
+
print_guest_block_msg,
|
| 26 |
+
print_rich_table,
|
| 27 |
+
remove_file,
|
| 28 |
+
system_clear,
|
| 29 |
+
validate_register_key,
|
| 30 |
+
)
|
| 31 |
+
from openbb_cli.session import Session
|
| 32 |
+
from prompt_toolkit.formatted_text import HTML
|
| 33 |
+
from prompt_toolkit.styles import Style
|
| 34 |
+
|
| 35 |
+
# pylint: disable=C0301,C0302,R0902,global-statement,too-many-boolean-expressions
|
| 36 |
+
# pylint: disable=R0912
|
| 37 |
+
|
| 38 |
+
controllers: Dict[str, Any] = {}
|
| 39 |
+
session = Session()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# TODO: We should try to avoid these global variables
|
| 43 |
+
RECORD_SESSION = False
|
| 44 |
+
RECORD_SESSION_LOCAL_ONLY = False
|
| 45 |
+
SESSION_RECORDED = list()
|
| 46 |
+
SESSION_RECORDED_NAME = ""
|
| 47 |
+
SESSION_RECORDED_DESCRIPTION = ""
|
| 48 |
+
SESSION_RECORDED_TAGS = ""
|
| 49 |
+
SESSION_RECORDED_PUBLIC = False
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class BaseController(metaclass=ABCMeta):
|
| 53 |
+
"""Base class for a cli controller."""
|
| 54 |
+
|
| 55 |
+
CHOICES_COMMON = [
|
| 56 |
+
"cls",
|
| 57 |
+
"home",
|
| 58 |
+
"h",
|
| 59 |
+
"?",
|
| 60 |
+
"help",
|
| 61 |
+
"q",
|
| 62 |
+
"quit",
|
| 63 |
+
"..",
|
| 64 |
+
"e",
|
| 65 |
+
"exit",
|
| 66 |
+
"r",
|
| 67 |
+
"reset",
|
| 68 |
+
"stop",
|
| 69 |
+
"whoami",
|
| 70 |
+
"results",
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
CHOICES_COMMANDS: List[str] = []
|
| 74 |
+
CHOICES_MENUS: List[str] = []
|
| 75 |
+
NEWS_CHOICES: dict = {}
|
| 76 |
+
COMMAND_SEPARATOR = "/"
|
| 77 |
+
KEYS_MENU = "keys" + COMMAND_SEPARATOR
|
| 78 |
+
PATH: str = ""
|
| 79 |
+
FILE_PATH: str = ""
|
| 80 |
+
CHOICES_GENERATION = False
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def choices_default(self):
|
| 84 |
+
"""Return the default choices."""
|
| 85 |
+
choices = (
|
| 86 |
+
build_controller_choice_map(controller=self)
|
| 87 |
+
if self.CHOICES_GENERATION
|
| 88 |
+
else {}
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
return choices
|
| 92 |
+
|
| 93 |
+
def __init__(self, queue: Optional[List[str]] = None) -> None:
|
| 94 |
+
"""Create the base class for any controller in the codebase.
|
| 95 |
+
|
| 96 |
+
Used to simplify the creation of menus.
|
| 97 |
+
|
| 98 |
+
queue: List[str]
|
| 99 |
+
The current queue of jobs to process separated by "/"
|
| 100 |
+
E.g. /stocks/load gme/dps/sidtc/../exit
|
| 101 |
+
"""
|
| 102 |
+
self.check_path()
|
| 103 |
+
self.path = [x for x in self.PATH.split("/") if x != ""]
|
| 104 |
+
self.queue = (
|
| 105 |
+
self.parse_input(an_input="/".join(queue))
|
| 106 |
+
if (queue and self.PATH != "/")
|
| 107 |
+
else list()
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
controller_choices = self.CHOICES_COMMANDS + self.CHOICES_MENUS
|
| 111 |
+
if controller_choices:
|
| 112 |
+
self.controller_choices = controller_choices + self.CHOICES_COMMON
|
| 113 |
+
else:
|
| 114 |
+
self.controller_choices = self.CHOICES_COMMON
|
| 115 |
+
|
| 116 |
+
self.completer: Union[None, NestedCompleter] = None
|
| 117 |
+
|
| 118 |
+
self.parser = argparse.ArgumentParser(
|
| 119 |
+
add_help=False,
|
| 120 |
+
prog=self.path[-1] if self.PATH != "/" else "cli",
|
| 121 |
+
)
|
| 122 |
+
self.parser.exit_on_error = False # type: ignore
|
| 123 |
+
self.parser.add_argument("cmd", choices=self.controller_choices)
|
| 124 |
+
|
| 125 |
+
def update_completer(self, choices) -> None:
|
| 126 |
+
"""Update the completer with new choices."""
|
| 127 |
+
if session.prompt_session and session.settings.USE_PROMPT_TOOLKIT:
|
| 128 |
+
self.completer = NestedCompleter.from_nested_dict(choices)
|
| 129 |
+
|
| 130 |
+
def check_path(self) -> None:
|
| 131 |
+
"""Check if command path is valid."""
|
| 132 |
+
path = self.PATH
|
| 133 |
+
if path[0] != "/":
|
| 134 |
+
raise ValueError("Path must begin with a '/' character.")
|
| 135 |
+
if path[-1] != "/":
|
| 136 |
+
raise ValueError("Path must end with a '/' character.")
|
| 137 |
+
if not re.match("^[a-z/]*$", path):
|
| 138 |
+
raise ValueError(
|
| 139 |
+
"Path must only contain lowercase letters and '/' characters."
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
def load_class(self, class_ins, *args, **kwargs):
|
| 143 |
+
"""Check for an existing instance of the controller before creating a new one."""
|
| 144 |
+
self.save_class()
|
| 145 |
+
arguments = len(args) + len(kwargs)
|
| 146 |
+
|
| 147 |
+
if class_ins.PATH in controllers and arguments == 1:
|
| 148 |
+
old_class = controllers[class_ins.PATH]
|
| 149 |
+
old_class.queue = self.queue
|
| 150 |
+
return old_class.menu()
|
| 151 |
+
return class_ins(*args, **kwargs).menu()
|
| 152 |
+
|
| 153 |
+
def save_class(self) -> None:
|
| 154 |
+
"""Save the current instance of the class to be loaded later."""
|
| 155 |
+
controllers[self.PATH] = self
|
| 156 |
+
|
| 157 |
+
def custom_reset(self) -> List[str]:
|
| 158 |
+
"""Implement custom reset.
|
| 159 |
+
|
| 160 |
+
This will be replaced by any children with custom_reset functions.
|
| 161 |
+
"""
|
| 162 |
+
return []
|
| 163 |
+
|
| 164 |
+
@abstractmethod
|
| 165 |
+
def print_help(self) -> None:
|
| 166 |
+
"""Print help placeholder."""
|
| 167 |
+
raise NotImplementedError("Must override print_help.")
|
| 168 |
+
|
| 169 |
+
def parse_input(self, an_input: str) -> list:
|
| 170 |
+
"""Parse controller input.
|
| 171 |
+
|
| 172 |
+
Splits the command chain from user input into a list of individual commands
|
| 173 |
+
while respecting the forward slash in the command arguments.
|
| 174 |
+
|
| 175 |
+
In the default scenario only unix-like paths are handles by the parser.
|
| 176 |
+
Override this function in the controller classes that inherit from this one to
|
| 177 |
+
resolve edge cases specific to command arguments on those controllers.
|
| 178 |
+
|
| 179 |
+
When handling edge cases add additional regular expressions to the list.
|
| 180 |
+
|
| 181 |
+
Parameters
|
| 182 |
+
----------
|
| 183 |
+
an_input : str
|
| 184 |
+
User input string
|
| 185 |
+
|
| 186 |
+
Returns
|
| 187 |
+
----------
|
| 188 |
+
list
|
| 189 |
+
Command queue as list
|
| 190 |
+
"""
|
| 191 |
+
custom_filters: list = []
|
| 192 |
+
commands = parse_and_split_input(
|
| 193 |
+
an_input=an_input, custom_filters=custom_filters
|
| 194 |
+
)
|
| 195 |
+
return commands
|
| 196 |
+
|
| 197 |
+
def switch(self, an_input: str) -> List[str]:
|
| 198 |
+
"""Process and dispatch input.
|
| 199 |
+
|
| 200 |
+
Returns
|
| 201 |
+
----------
|
| 202 |
+
List[str]
|
| 203 |
+
list of commands in the queue to execute
|
| 204 |
+
"""
|
| 205 |
+
actions = self.parse_input(an_input)
|
| 206 |
+
|
| 207 |
+
if an_input and an_input != "reset":
|
| 208 |
+
session.console.print()
|
| 209 |
+
|
| 210 |
+
# Empty command
|
| 211 |
+
if len(actions) == 0:
|
| 212 |
+
pass
|
| 213 |
+
|
| 214 |
+
# Navigation slash is being used first split commands
|
| 215 |
+
elif len(actions) > 1:
|
| 216 |
+
# Absolute path is specified
|
| 217 |
+
if not actions[0]:
|
| 218 |
+
actions[0] = "home"
|
| 219 |
+
|
| 220 |
+
# Add all instructions to the queue
|
| 221 |
+
for cmd in actions[::-1]:
|
| 222 |
+
if cmd:
|
| 223 |
+
self.queue.insert(0, cmd)
|
| 224 |
+
|
| 225 |
+
# Single command fed, process
|
| 226 |
+
else:
|
| 227 |
+
try:
|
| 228 |
+
(known_args, other_args) = self.parser.parse_known_args(
|
| 229 |
+
shlex.split(an_input)
|
| 230 |
+
)
|
| 231 |
+
except Exception as exc:
|
| 232 |
+
raise SystemExit from exc
|
| 233 |
+
|
| 234 |
+
if RECORD_SESSION:
|
| 235 |
+
SESSION_RECORDED.append(an_input)
|
| 236 |
+
|
| 237 |
+
# Redirect commands to their correct functions
|
| 238 |
+
if known_args.cmd:
|
| 239 |
+
if known_args.cmd in ("..", "q"):
|
| 240 |
+
known_args.cmd = "quit"
|
| 241 |
+
elif known_args.cmd in ("e"):
|
| 242 |
+
known_args.cmd = "exit"
|
| 243 |
+
elif known_args.cmd in ("?", "h"):
|
| 244 |
+
known_args.cmd = "help"
|
| 245 |
+
elif known_args.cmd == "r":
|
| 246 |
+
known_args.cmd = "reset"
|
| 247 |
+
|
| 248 |
+
getattr(
|
| 249 |
+
self,
|
| 250 |
+
"call_" + known_args.cmd,
|
| 251 |
+
lambda _: "Command not recognized!",
|
| 252 |
+
)(other_args)
|
| 253 |
+
|
| 254 |
+
if (
|
| 255 |
+
an_input
|
| 256 |
+
and an_input != "reset"
|
| 257 |
+
and (
|
| 258 |
+
not self.queue or (self.queue and self.queue[0] not in ("quit", "help"))
|
| 259 |
+
)
|
| 260 |
+
):
|
| 261 |
+
session.console.print()
|
| 262 |
+
|
| 263 |
+
return self.queue
|
| 264 |
+
|
| 265 |
+
def call_cls(self, _) -> None:
|
| 266 |
+
"""Process cls command."""
|
| 267 |
+
system_clear()
|
| 268 |
+
|
| 269 |
+
def call_home(self, _) -> None:
|
| 270 |
+
"""Process home command."""
|
| 271 |
+
self.save_class()
|
| 272 |
+
if self.PATH.count("/") == 1 and session.settings.ENABLE_EXIT_AUTO_HELP:
|
| 273 |
+
self.print_help()
|
| 274 |
+
for _ in range(self.PATH.count("/") - 1):
|
| 275 |
+
self.queue.insert(0, "quit")
|
| 276 |
+
|
| 277 |
+
def call_help(self, _) -> None:
|
| 278 |
+
"""Process help command."""
|
| 279 |
+
self.print_help()
|
| 280 |
+
|
| 281 |
+
def call_quit(self, _) -> None:
|
| 282 |
+
"""Process quit menu command."""
|
| 283 |
+
self.save_class()
|
| 284 |
+
self.queue.insert(0, "quit")
|
| 285 |
+
|
| 286 |
+
def call_exit(self, _) -> None:
|
| 287 |
+
# Not sure how to handle controller loading here
|
| 288 |
+
"""Process exit cli command."""
|
| 289 |
+
self.save_class()
|
| 290 |
+
for _ in range(self.PATH.count("/")):
|
| 291 |
+
self.queue.insert(0, "quit")
|
| 292 |
+
|
| 293 |
+
if not session.is_local():
|
| 294 |
+
remove_file(
|
| 295 |
+
Path(session.user.preferences.export_directory, "routines", "hub")
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
def call_reset(self, _) -> None:
|
| 299 |
+
"""Process reset command.
|
| 300 |
+
|
| 301 |
+
If you would like to have customization in the reset process define a method
|
| 302 |
+
`custom_reset` in the child class.
|
| 303 |
+
"""
|
| 304 |
+
self.save_class()
|
| 305 |
+
if self.PATH != "/":
|
| 306 |
+
if self.custom_reset():
|
| 307 |
+
self.queue = self.custom_reset() + self.queue
|
| 308 |
+
else:
|
| 309 |
+
for val in self.path[::-1]:
|
| 310 |
+
self.queue.insert(0, val)
|
| 311 |
+
self.queue.insert(0, "reset")
|
| 312 |
+
for _ in range(len(self.path)):
|
| 313 |
+
self.queue.insert(0, "quit")
|
| 314 |
+
|
| 315 |
+
def call_record(self, other_args) -> None:
|
| 316 |
+
"""Process record command."""
|
| 317 |
+
parser = argparse.ArgumentParser(
|
| 318 |
+
add_help=False,
|
| 319 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 320 |
+
prog="record",
|
| 321 |
+
description="Start recording session into .openbb routine file",
|
| 322 |
+
)
|
| 323 |
+
parser.add_argument(
|
| 324 |
+
"-n",
|
| 325 |
+
"--name",
|
| 326 |
+
action="store",
|
| 327 |
+
dest="name",
|
| 328 |
+
type=str,
|
| 329 |
+
default="",
|
| 330 |
+
help="Routine title name to be saved - only use characters, digits and whitespaces.",
|
| 331 |
+
nargs="+",
|
| 332 |
+
)
|
| 333 |
+
parser.add_argument(
|
| 334 |
+
"-d",
|
| 335 |
+
"--description",
|
| 336 |
+
type=str,
|
| 337 |
+
dest="description",
|
| 338 |
+
help="The description of the routine",
|
| 339 |
+
default=f"Routine recorded at {datetime.now().strftime('%H:%M')} from the OpenBB Platform CLI",
|
| 340 |
+
nargs="+",
|
| 341 |
+
)
|
| 342 |
+
parser.add_argument(
|
| 343 |
+
"--tag1",
|
| 344 |
+
type=str,
|
| 345 |
+
dest="tag1",
|
| 346 |
+
help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
|
| 347 |
+
default="",
|
| 348 |
+
nargs="+",
|
| 349 |
+
)
|
| 350 |
+
parser.add_argument(
|
| 351 |
+
"--tag2",
|
| 352 |
+
type=str,
|
| 353 |
+
dest="tag2",
|
| 354 |
+
help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
|
| 355 |
+
default="",
|
| 356 |
+
nargs="+",
|
| 357 |
+
)
|
| 358 |
+
parser.add_argument(
|
| 359 |
+
"--tag3",
|
| 360 |
+
type=str,
|
| 361 |
+
dest="tag3",
|
| 362 |
+
help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
|
| 363 |
+
default="",
|
| 364 |
+
nargs="+",
|
| 365 |
+
)
|
| 366 |
+
parser.add_argument(
|
| 367 |
+
"-p",
|
| 368 |
+
"--public",
|
| 369 |
+
dest="public",
|
| 370 |
+
action="store_true",
|
| 371 |
+
help="Whether the routine should be public or not",
|
| 372 |
+
default=False,
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
if other_args and "-" not in other_args[0][0]:
|
| 376 |
+
other_args.insert(0, "-n")
|
| 377 |
+
|
| 378 |
+
ns_parser, _ = self.parse_simple_args(parser, other_args)
|
| 379 |
+
|
| 380 |
+
if ns_parser:
|
| 381 |
+
if not ns_parser.name:
|
| 382 |
+
session.console.print(
|
| 383 |
+
"[red]Set a routine title by using the '-n' flag. E.g. 'record -n Morning routine'[/red]"
|
| 384 |
+
)
|
| 385 |
+
return
|
| 386 |
+
|
| 387 |
+
tag1 = (
|
| 388 |
+
" ".join(ns_parser.tag1)
|
| 389 |
+
if isinstance(ns_parser.tag1, list)
|
| 390 |
+
else ns_parser.tag1
|
| 391 |
+
)
|
| 392 |
+
if tag1 and tag1 not in SCRIPT_TAGS:
|
| 393 |
+
session.console.print(
|
| 394 |
+
f"[red]The parameter 'tag1' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
|
| 395 |
+
)
|
| 396 |
+
return
|
| 397 |
+
|
| 398 |
+
tag2 = (
|
| 399 |
+
" ".join(ns_parser.tag2)
|
| 400 |
+
if isinstance(ns_parser.tag2, list)
|
| 401 |
+
else ns_parser.tag2
|
| 402 |
+
)
|
| 403 |
+
if tag2 and tag2 not in SCRIPT_TAGS:
|
| 404 |
+
session.console.print(
|
| 405 |
+
f"[red]The parameter 'tag2' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
|
| 406 |
+
)
|
| 407 |
+
return
|
| 408 |
+
|
| 409 |
+
tag3 = (
|
| 410 |
+
" ".join(ns_parser.tag3)
|
| 411 |
+
if isinstance(ns_parser.tag3, list)
|
| 412 |
+
else ns_parser.tag3
|
| 413 |
+
)
|
| 414 |
+
if tag3 and tag3 not in SCRIPT_TAGS:
|
| 415 |
+
session.console.print(
|
| 416 |
+
f"[red]The parameter 'tag3' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
|
| 417 |
+
)
|
| 418 |
+
return
|
| 419 |
+
|
| 420 |
+
if session.is_local():
|
| 421 |
+
session.console.print(
|
| 422 |
+
"[red]Recording session to the OpenBB Hub is not supported in guest mode.[/red]"
|
| 423 |
+
)
|
| 424 |
+
session.console.print(
|
| 425 |
+
"\n[yellow]Visit the OpenBB Hub to register: http://my.openbb.co[/yellow]"
|
| 426 |
+
)
|
| 427 |
+
session.console.print(
|
| 428 |
+
"\n[yellow]Your routine will be saved locally.[/yellow]\n"
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
# Check if title has a valid format
|
| 432 |
+
title = " ".join(ns_parser.name) if ns_parser.name else ""
|
| 433 |
+
pattern = re.compile(r"^[a-zA-Z0-9\s]+$")
|
| 434 |
+
if not pattern.match(title):
|
| 435 |
+
session.console.print(
|
| 436 |
+
f"[red]Title '{title}' has invalid format. Please use only digits, characters and whitespaces.[/]"
|
| 437 |
+
)
|
| 438 |
+
return
|
| 439 |
+
|
| 440 |
+
global RECORD_SESSION # noqa: PLW0603
|
| 441 |
+
global RECORD_SESSION_LOCAL_ONLY # noqa: PLW0603
|
| 442 |
+
global SESSION_RECORDED_NAME # noqa: PLW0603
|
| 443 |
+
global SESSION_RECORDED_DESCRIPTION # noqa: PLW0603
|
| 444 |
+
global SESSION_RECORDED_TAGS # noqa: PLW0603
|
| 445 |
+
global SESSION_RECORDED_PUBLIC # noqa: PLW0603
|
| 446 |
+
|
| 447 |
+
RECORD_SESSION_LOCAL_ONLY = session.is_local()
|
| 448 |
+
RECORD_SESSION = True
|
| 449 |
+
SESSION_RECORDED_NAME = title
|
| 450 |
+
SESSION_RECORDED_DESCRIPTION = (
|
| 451 |
+
" ".join(ns_parser.description)
|
| 452 |
+
if isinstance(ns_parser.description, list)
|
| 453 |
+
else ns_parser.description
|
| 454 |
+
)
|
| 455 |
+
SESSION_RECORDED_TAGS = tag1 if tag1 else ""
|
| 456 |
+
SESSION_RECORDED_TAGS += "," + tag2 if tag2 else ""
|
| 457 |
+
SESSION_RECORDED_TAGS += "," + tag3 if tag3 else ""
|
| 458 |
+
|
| 459 |
+
SESSION_RECORDED_PUBLIC = ns_parser.public
|
| 460 |
+
|
| 461 |
+
session.console.print(
|
| 462 |
+
f"[green]The routine '{title}' is successfully being recorded.[/green]"
|
| 463 |
+
)
|
| 464 |
+
session.console.print(
|
| 465 |
+
"\n[yellow]Remember to run 'stop' command when you are done!\n[/yellow]"
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
def call_stop(self, other_args) -> None:
|
| 469 |
+
"""Process stop command."""
|
| 470 |
+
parser = argparse.ArgumentParser(
|
| 471 |
+
add_help=False,
|
| 472 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 473 |
+
prog="stop",
|
| 474 |
+
description="Stop recording session into .openbb routine file",
|
| 475 |
+
)
|
| 476 |
+
# This is only for auto-completion purposes
|
| 477 |
+
_, _ = self.parse_simple_args(parser, other_args)
|
| 478 |
+
|
| 479 |
+
if "-h" not in other_args and "--help" not in other_args:
|
| 480 |
+
global RECORD_SESSION # noqa: PLW0603
|
| 481 |
+
global SESSION_RECORDED # noqa: PLW0603
|
| 482 |
+
|
| 483 |
+
if not RECORD_SESSION:
|
| 484 |
+
session.console.print(
|
| 485 |
+
"[red]There is no session being recorded. Start one using the command 'record'[/red]\n"
|
| 486 |
+
)
|
| 487 |
+
elif len(SESSION_RECORDED) < 5:
|
| 488 |
+
session.console.print(
|
| 489 |
+
"[red]Run at least 4 commands before stopping recording a session.[/red]\n"
|
| 490 |
+
)
|
| 491 |
+
else:
|
| 492 |
+
current_user = session.user
|
| 493 |
+
|
| 494 |
+
# Check if the user just wants to store routine locally
|
| 495 |
+
# This works regardless of whether they are logged in or not
|
| 496 |
+
if RECORD_SESSION_LOCAL_ONLY:
|
| 497 |
+
# Whitespaces are replaced by underscores and an .openbb extension is added
|
| 498 |
+
title_for_local_storage = (
|
| 499 |
+
SESSION_RECORDED_NAME.replace(" ", "_") + ".openbb"
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
routine_file = os.path.join(
|
| 503 |
+
f"{current_user.preferences.export_directory}/routines",
|
| 504 |
+
title_for_local_storage,
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
# If file already exists, add a timestamp to the name
|
| 508 |
+
if os.path.isfile(routine_file):
|
| 509 |
+
i = session.console.input(
|
| 510 |
+
"A local routine with the same name already exists, "
|
| 511 |
+
"do you want to override it? (y/n): "
|
| 512 |
+
)
|
| 513 |
+
session.console.print("")
|
| 514 |
+
while i.lower() not in ["y", "yes", "n", "no"]:
|
| 515 |
+
i = session.console.input("Select 'y' or 'n' to proceed: ")
|
| 516 |
+
session.console.print("")
|
| 517 |
+
|
| 518 |
+
if i.lower() in ["n", "no"]:
|
| 519 |
+
new_name = (
|
| 520 |
+
datetime.now().strftime("%Y%m%d_%H%M%S_")
|
| 521 |
+
+ title_for_local_storage
|
| 522 |
+
)
|
| 523 |
+
routine_file = os.path.join(
|
| 524 |
+
current_user.preferences.export_directory,
|
| 525 |
+
"routines",
|
| 526 |
+
new_name,
|
| 527 |
+
)
|
| 528 |
+
session.console.print(
|
| 529 |
+
f"[yellow]The routine name has been updated to '{new_name}'[/yellow]\n"
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
# Writing to file
|
| 533 |
+
Path(os.path.dirname(routine_file)).mkdir(
|
| 534 |
+
parents=True, exist_ok=True
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
with open(routine_file, "w") as file1:
|
| 538 |
+
lines = ["# OpenBB Platform CLI - Routine", "\n"]
|
| 539 |
+
|
| 540 |
+
username = getattr(
|
| 541 |
+
session.user.profile.hub_session, "username", "local"
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
lines += (
|
| 545 |
+
[f"# Author: {username}", "\n\n"] if username else ["\n"]
|
| 546 |
+
)
|
| 547 |
+
lines += [
|
| 548 |
+
f"# Title: {SESSION_RECORDED_NAME}",
|
| 549 |
+
"\n",
|
| 550 |
+
f"# Tags: {SESSION_RECORDED_TAGS}",
|
| 551 |
+
"\n\n",
|
| 552 |
+
f"# Description: {SESSION_RECORDED_DESCRIPTION}",
|
| 553 |
+
"\n\n",
|
| 554 |
+
]
|
| 555 |
+
lines += [c + "\n" for c in SESSION_RECORDED[:-1]]
|
| 556 |
+
# Writing data to a file
|
| 557 |
+
file1.writelines(lines)
|
| 558 |
+
|
| 559 |
+
session.console.print(
|
| 560 |
+
f"[green]Your routine has been recorded and saved here: {routine_file}[/green]\n"
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
# If user doesn't specify they want to store routine locally
|
| 564 |
+
# Confirm that the user is logged in
|
| 565 |
+
elif not session.is_local():
|
| 566 |
+
routine = "\n".join(SESSION_RECORDED[:-1])
|
| 567 |
+
hub_session = current_user.profile.hub_session
|
| 568 |
+
|
| 569 |
+
if routine is not None:
|
| 570 |
+
auth_header = (
|
| 571 |
+
f"{hub_session.token_type} {hub_session.access_token.get_secret_value()}"
|
| 572 |
+
if hub_session
|
| 573 |
+
else None
|
| 574 |
+
)
|
| 575 |
+
kwargs = {
|
| 576 |
+
"auth_header": auth_header,
|
| 577 |
+
"name": SESSION_RECORDED_NAME,
|
| 578 |
+
"description": SESSION_RECORDED_DESCRIPTION,
|
| 579 |
+
"routine": routine,
|
| 580 |
+
"tags": SESSION_RECORDED_TAGS,
|
| 581 |
+
"public": SESSION_RECORDED_PUBLIC,
|
| 582 |
+
}
|
| 583 |
+
response = upload_routine(**kwargs) # type: ignore
|
| 584 |
+
if response is not None and response.status_code == 409:
|
| 585 |
+
i = session.console.input(
|
| 586 |
+
"A routine with the same name already exists, "
|
| 587 |
+
"do you want to replace it? (y/n): "
|
| 588 |
+
)
|
| 589 |
+
session.console.print("")
|
| 590 |
+
if i.lower() in ["y", "yes"]:
|
| 591 |
+
kwargs["override"] = True # type: ignore
|
| 592 |
+
response = upload_routine(**kwargs) # type: ignore
|
| 593 |
+
else:
|
| 594 |
+
session.console.print("[info]Aborted.[/info]")
|
| 595 |
+
|
| 596 |
+
# Clear session to be recorded again
|
| 597 |
+
RECORD_SESSION = False
|
| 598 |
+
SESSION_RECORDED = list()
|
| 599 |
+
|
| 600 |
+
def call_whoami(self, other_args: List[str]) -> None:
|
| 601 |
+
"""Process whoami command."""
|
| 602 |
+
parser = argparse.ArgumentParser(
|
| 603 |
+
add_help=False,
|
| 604 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 605 |
+
prog="whoami",
|
| 606 |
+
description="Show current user",
|
| 607 |
+
)
|
| 608 |
+
ns_parser, _ = self.parse_simple_args(parser, other_args)
|
| 609 |
+
|
| 610 |
+
if ns_parser:
|
| 611 |
+
current_user = session.user
|
| 612 |
+
local_user = session.is_local()
|
| 613 |
+
if not local_user:
|
| 614 |
+
hub_session = current_user.profile.hub_session
|
| 615 |
+
session.console.print(
|
| 616 |
+
f"[info]email:[/info] {hub_session.email if hub_session else 'N/A'}"
|
| 617 |
+
)
|
| 618 |
+
session.console.print(
|
| 619 |
+
f"[info]uuid:[/info] {hub_session.user_uuid if hub_session else 'N/A'}"
|
| 620 |
+
)
|
| 621 |
+
else:
|
| 622 |
+
print_guest_block_msg()
|
| 623 |
+
|
| 624 |
+
def call_results(self, other_args: List[str]):
|
| 625 |
+
"""Process results command."""
|
| 626 |
+
parser = argparse.ArgumentParser(
|
| 627 |
+
add_help=False,
|
| 628 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 629 |
+
prog="results",
|
| 630 |
+
description="Process results command. This command displays a registry of "
|
| 631 |
+
"'OBBjects' where all execution results are stored. "
|
| 632 |
+
"It is organized as a stack, with the most recent result at index 0.",
|
| 633 |
+
)
|
| 634 |
+
parser.add_argument("--index", dest="index", help="Index of the result.")
|
| 635 |
+
parser.add_argument("--key", dest="key", help="Key of the result.")
|
| 636 |
+
parser.add_argument(
|
| 637 |
+
"--chart", action="store_true", dest="chart", help="Display chart."
|
| 638 |
+
)
|
| 639 |
+
parser.add_argument(
|
| 640 |
+
"--export",
|
| 641 |
+
default="",
|
| 642 |
+
type=check_file_type_saved(["csv", "json", "xlsx", "png", "jpg"]),
|
| 643 |
+
dest="export",
|
| 644 |
+
help="Export raw data into csv, json, xlsx and figure into png or jpg.",
|
| 645 |
+
nargs="+",
|
| 646 |
+
)
|
| 647 |
+
parser.add_argument(
|
| 648 |
+
"--sheet-name",
|
| 649 |
+
dest="sheet_name",
|
| 650 |
+
default=None,
|
| 651 |
+
nargs="+",
|
| 652 |
+
help="Name of excel sheet to save data to. Only valid for .xlsx files.",
|
| 653 |
+
)
|
| 654 |
+
|
| 655 |
+
ns_parser, unknown_args = self.parse_simple_args(
|
| 656 |
+
parser, other_args, unknown_args=True
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
if ns_parser:
|
| 660 |
+
kwargs = parse_unknown_args_to_dict(unknown_args)
|
| 661 |
+
if not ns_parser.index and not ns_parser.key:
|
| 662 |
+
results = session.obbject_registry.all
|
| 663 |
+
if results:
|
| 664 |
+
df = pd.DataFrame.from_dict(results, orient="index")
|
| 665 |
+
print_rich_table(
|
| 666 |
+
df,
|
| 667 |
+
show_index=True,
|
| 668 |
+
index_name="stack index",
|
| 669 |
+
title="OBBject Results",
|
| 670 |
+
)
|
| 671 |
+
else:
|
| 672 |
+
session.console.print("[info]No results found.[/info]")
|
| 673 |
+
elif ns_parser.index:
|
| 674 |
+
try:
|
| 675 |
+
index = int(ns_parser.index)
|
| 676 |
+
obbject = session.obbject_registry.get(index)
|
| 677 |
+
if obbject:
|
| 678 |
+
handle_obbject_display(
|
| 679 |
+
obbject=obbject,
|
| 680 |
+
chart=ns_parser.chart,
|
| 681 |
+
export=ns_parser.export,
|
| 682 |
+
sheet_name=ns_parser.sheet_name,
|
| 683 |
+
**kwargs,
|
| 684 |
+
)
|
| 685 |
+
else:
|
| 686 |
+
session.console.print(
|
| 687 |
+
f"[info]No result found at index {index}.[/info]"
|
| 688 |
+
)
|
| 689 |
+
except ValueError:
|
| 690 |
+
session.console.print(
|
| 691 |
+
f"[red]Index must be an integer, not '{ns_parser.index}'.[/red]"
|
| 692 |
+
)
|
| 693 |
+
elif ns_parser.key:
|
| 694 |
+
obbject = session.obbject_registry.get(ns_parser.key)
|
| 695 |
+
if obbject:
|
| 696 |
+
handle_obbject_display(
|
| 697 |
+
obbject=obbject,
|
| 698 |
+
chart=ns_parser.chart,
|
| 699 |
+
export=ns_parser.export,
|
| 700 |
+
sheet_name=ns_parser.sheet_name,
|
| 701 |
+
**kwargs,
|
| 702 |
+
)
|
| 703 |
+
else:
|
| 704 |
+
session.console.print(
|
| 705 |
+
f"[info]No result found with key '{ns_parser.key}'.[/info]"
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
@staticmethod
|
| 709 |
+
def parse_simple_args(
|
| 710 |
+
parser: argparse.ArgumentParser,
|
| 711 |
+
other_args: List[str],
|
| 712 |
+
unknown_args: bool = False,
|
| 713 |
+
) -> Tuple[Optional[argparse.Namespace], Optional[List[str]]]:
|
| 714 |
+
"""Parse list of arguments into the supplied parser.
|
| 715 |
+
|
| 716 |
+
Parameters
|
| 717 |
+
----------
|
| 718 |
+
parser: argparse.ArgumentParser
|
| 719 |
+
Parser with predefined arguments
|
| 720 |
+
other_args: List[str]
|
| 721 |
+
List of arguments to parse
|
| 722 |
+
unknown_args: bool
|
| 723 |
+
Flag to indicate if unknown arguments should be returned
|
| 724 |
+
|
| 725 |
+
Returns
|
| 726 |
+
-------
|
| 727 |
+
ns_parser: argparse.Namespace
|
| 728 |
+
Namespace with parsed arguments
|
| 729 |
+
l_unknown_args: List[str]
|
| 730 |
+
List of unknown arguments
|
| 731 |
+
"""
|
| 732 |
+
parser.add_argument(
|
| 733 |
+
"-h", "--help", action="store_true", help="show this help message"
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
if session.settings.USE_CLEAR_AFTER_CMD:
|
| 737 |
+
system_clear()
|
| 738 |
+
|
| 739 |
+
try:
|
| 740 |
+
(ns_parser, l_unknown_args) = parser.parse_known_args(other_args)
|
| 741 |
+
except SystemExit:
|
| 742 |
+
# In case the command has required argument that isn't specified
|
| 743 |
+
session.console.print("\n")
|
| 744 |
+
return None, None
|
| 745 |
+
|
| 746 |
+
if ns_parser.help:
|
| 747 |
+
txt_help = parser.format_help()
|
| 748 |
+
session.console.print(f"[help]{txt_help}[/help]")
|
| 749 |
+
return None, None
|
| 750 |
+
|
| 751 |
+
if l_unknown_args and not unknown_args:
|
| 752 |
+
session.console.print(
|
| 753 |
+
f"The following args couldn't be interpreted: {l_unknown_args}\n"
|
| 754 |
+
)
|
| 755 |
+
return ns_parser, l_unknown_args
|
| 756 |
+
|
| 757 |
+
@classmethod
|
| 758 |
+
def parse_known_args_and_warn(
|
| 759 |
+
cls,
|
| 760 |
+
parser: argparse.ArgumentParser,
|
| 761 |
+
other_args: List[str],
|
| 762 |
+
export_allowed: Literal[
|
| 763 |
+
"no_export", "raw_data_only", "figures_only", "raw_data_and_figures"
|
| 764 |
+
] = "no_export",
|
| 765 |
+
raw: bool = False,
|
| 766 |
+
limit: int = 0,
|
| 767 |
+
):
|
| 768 |
+
"""Parse list of arguments into the supplied parser.
|
| 769 |
+
|
| 770 |
+
Parameters
|
| 771 |
+
----------
|
| 772 |
+
parser: argparse.ArgumentParser
|
| 773 |
+
Parser with predefined arguments
|
| 774 |
+
other_args: List[str]
|
| 775 |
+
list of arguments to parse
|
| 776 |
+
export_allowed: Literal["no_export", "raw_data_only", "figures_only", "raw_data_and_figures"]
|
| 777 |
+
Export options
|
| 778 |
+
raw: bool
|
| 779 |
+
Add the --raw flag
|
| 780 |
+
limit: int
|
| 781 |
+
Add a --limit flag with this number default
|
| 782 |
+
|
| 783 |
+
Returns
|
| 784 |
+
----------
|
| 785 |
+
ns_parser:
|
| 786 |
+
Namespace with parsed arguments
|
| 787 |
+
"""
|
| 788 |
+
parser.add_argument(
|
| 789 |
+
"-h", "--help", action="store_true", help="show this help message"
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
if export_allowed != "no_export":
|
| 793 |
+
choices_export = []
|
| 794 |
+
help_export = "Does not export!"
|
| 795 |
+
|
| 796 |
+
if export_allowed == "raw_data_only":
|
| 797 |
+
choices_export = ["csv", "json", "xlsx"]
|
| 798 |
+
help_export = "Export raw data into csv, json or xlsx."
|
| 799 |
+
elif export_allowed == "figures_only":
|
| 800 |
+
choices_export = ["png", "jpg"]
|
| 801 |
+
help_export = "Export figure into png or jpg."
|
| 802 |
+
else:
|
| 803 |
+
choices_export = ["csv", "json", "xlsx", "png", "jpg"]
|
| 804 |
+
help_export = (
|
| 805 |
+
"Export raw data into csv, json, xlsx and figure into png or jpg."
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
parser.add_argument(
|
| 809 |
+
"--export",
|
| 810 |
+
default="",
|
| 811 |
+
type=check_file_type_saved(choices_export),
|
| 812 |
+
dest="export",
|
| 813 |
+
help=help_export,
|
| 814 |
+
nargs="+",
|
| 815 |
+
)
|
| 816 |
+
|
| 817 |
+
# If excel is an option, add the sheet name
|
| 818 |
+
if export_allowed in [
|
| 819 |
+
"raw_data_only",
|
| 820 |
+
"raw_data_and_figures",
|
| 821 |
+
]:
|
| 822 |
+
parser.add_argument(
|
| 823 |
+
"--sheet-name",
|
| 824 |
+
dest="sheet_name",
|
| 825 |
+
default=None,
|
| 826 |
+
nargs="+",
|
| 827 |
+
help="Name of excel sheet to save data to. Only valid for .xlsx files.",
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
if raw:
|
| 831 |
+
parser.add_argument(
|
| 832 |
+
"--raw",
|
| 833 |
+
dest="raw",
|
| 834 |
+
action="store_true",
|
| 835 |
+
default=False,
|
| 836 |
+
help="Flag to display raw data",
|
| 837 |
+
)
|
| 838 |
+
if limit > 0:
|
| 839 |
+
parser.add_argument(
|
| 840 |
+
"-l",
|
| 841 |
+
"--limit",
|
| 842 |
+
dest="limit",
|
| 843 |
+
default=limit,
|
| 844 |
+
help="Number of entries to show in data.",
|
| 845 |
+
type=check_positive,
|
| 846 |
+
)
|
| 847 |
+
|
| 848 |
+
parser.add_argument(
|
| 849 |
+
"--register_obbject",
|
| 850 |
+
dest="register_obbject",
|
| 851 |
+
action="store_false",
|
| 852 |
+
default=True,
|
| 853 |
+
help="Flag to store data in the OBBject registry, True by default.",
|
| 854 |
+
)
|
| 855 |
+
parser.add_argument(
|
| 856 |
+
"--register_key",
|
| 857 |
+
dest="register_key",
|
| 858 |
+
default="",
|
| 859 |
+
help="Key to reference data in the OBBject registry.",
|
| 860 |
+
type=validate_register_key,
|
| 861 |
+
)
|
| 862 |
+
|
| 863 |
+
if session.settings.USE_CLEAR_AFTER_CMD:
|
| 864 |
+
system_clear()
|
| 865 |
+
|
| 866 |
+
if "--help" in other_args or "-h" in other_args:
|
| 867 |
+
txt_help = parser.format_help() + "\n"
|
| 868 |
+
session.console.print(f"[help]{txt_help}[/help]")
|
| 869 |
+
return None
|
| 870 |
+
|
| 871 |
+
try:
|
| 872 |
+
# Determine the index of the routine arguments
|
| 873 |
+
routine_args_index = next(
|
| 874 |
+
(
|
| 875 |
+
i + 1
|
| 876 |
+
for i, arg in enumerate(other_args)
|
| 877 |
+
if arg in ("-i", "--input")
|
| 878 |
+
and "routine_args"
|
| 879 |
+
in [
|
| 880 |
+
action.dest
|
| 881 |
+
for action in parser._actions # pylint: disable=protected-access
|
| 882 |
+
]
|
| 883 |
+
),
|
| 884 |
+
-1,
|
| 885 |
+
)
|
| 886 |
+
# Split comma-separated arguments, except for the argument at routine_args_index
|
| 887 |
+
other_args = [
|
| 888 |
+
part
|
| 889 |
+
for index, arg in enumerate(other_args)
|
| 890 |
+
for part in (arg.split(",") if index != routine_args_index else [arg])
|
| 891 |
+
]
|
| 892 |
+
|
| 893 |
+
# Check if the action has optional choices, if yes, remove them
|
| 894 |
+
for action in parser._actions: # pylint: disable=protected-access
|
| 895 |
+
if hasattr(action, "optional_choices") and action.optional_choices:
|
| 896 |
+
action.choices = None
|
| 897 |
+
|
| 898 |
+
(ns_parser, l_unknown_args) = parser.parse_known_args(other_args)
|
| 899 |
+
|
| 900 |
+
if export_allowed in [
|
| 901 |
+
"raw_data_only",
|
| 902 |
+
"raw_data_and_figures",
|
| 903 |
+
]:
|
| 904 |
+
ns_parser.is_image = any(
|
| 905 |
+
ext in ns_parser.export for ext in ["png", "jpg"]
|
| 906 |
+
)
|
| 907 |
+
|
| 908 |
+
except SystemExit:
|
| 909 |
+
# In case the command has required argument that isn't specified
|
| 910 |
+
|
| 911 |
+
return None
|
| 912 |
+
|
| 913 |
+
if l_unknown_args:
|
| 914 |
+
session.console.print(
|
| 915 |
+
f"The following args couldn't be interpreted: {l_unknown_args}"
|
| 916 |
+
)
|
| 917 |
+
return ns_parser
|
| 918 |
+
|
| 919 |
+
def menu(self, custom_path_menu_above: str = ""):
|
| 920 |
+
"""Enter controller menu."""
|
| 921 |
+
settings = session.settings
|
| 922 |
+
an_input = "HELP_ME"
|
| 923 |
+
|
| 924 |
+
while True:
|
| 925 |
+
# There is a command in the queue
|
| 926 |
+
if self.queue and len(self.queue) > 0:
|
| 927 |
+
if self.queue[0] in ("q", "..", "quit"):
|
| 928 |
+
self.save_class()
|
| 929 |
+
# Go back to the root in order to go to the right directory because
|
| 930 |
+
# there was a jump between indirect menus
|
| 931 |
+
if custom_path_menu_above:
|
| 932 |
+
self.queue.insert(1, custom_path_menu_above)
|
| 933 |
+
|
| 934 |
+
if len(self.queue) > 1:
|
| 935 |
+
return self.queue[1:]
|
| 936 |
+
|
| 937 |
+
if settings.ENABLE_EXIT_AUTO_HELP:
|
| 938 |
+
return ["help"]
|
| 939 |
+
return []
|
| 940 |
+
|
| 941 |
+
# Consume 1 element from the queue
|
| 942 |
+
an_input = self.queue[0]
|
| 943 |
+
self.queue = self.queue[1:]
|
| 944 |
+
|
| 945 |
+
# Print location because this was an instruction and we want user to know the action
|
| 946 |
+
if (
|
| 947 |
+
an_input
|
| 948 |
+
and an_input != "home"
|
| 949 |
+
and an_input != "help"
|
| 950 |
+
and an_input.split(" ")[0] in self.controller_choices
|
| 951 |
+
):
|
| 952 |
+
session.console.print(
|
| 953 |
+
f"{get_flair_and_username()} {self.PATH} $ {an_input}"
|
| 954 |
+
)
|
| 955 |
+
|
| 956 |
+
# Get input command from user
|
| 957 |
+
else:
|
| 958 |
+
# Display help menu when entering on this menu from a level above
|
| 959 |
+
if an_input == "HELP_ME":
|
| 960 |
+
self.print_help()
|
| 961 |
+
|
| 962 |
+
try:
|
| 963 |
+
prompt_session = session.prompt_session
|
| 964 |
+
if prompt_session and settings.USE_PROMPT_TOOLKIT:
|
| 965 |
+
# Check if toolbar hint was enabled
|
| 966 |
+
if settings.TOOLBAR_HINT:
|
| 967 |
+
an_input = prompt_session.prompt(
|
| 968 |
+
f"{get_flair_and_username()} {self.PATH} $ ",
|
| 969 |
+
completer=self.completer,
|
| 970 |
+
search_ignore_case=True,
|
| 971 |
+
bottom_toolbar=HTML(
|
| 972 |
+
'<style bg="ansiblack" fg="ansiwhite">[h]</style> help menu '
|
| 973 |
+
'<style bg="ansiblack" fg="ansiwhite">[q]</style> return to previous menu '
|
| 974 |
+
'<style bg="ansiblack" fg="ansiwhite">[e]</style> exit the program '
|
| 975 |
+
'<style bg="ansiblack" fg="ansiwhite">[cmd -h]</style> '
|
| 976 |
+
"see usage and available options "
|
| 977 |
+
f"{self.path[-1].capitalize()} (cmd/menu) Documentation"
|
| 978 |
+
),
|
| 979 |
+
style=Style.from_dict(
|
| 980 |
+
{"bottom-toolbar": "#ffffff bg:#333333"}
|
| 981 |
+
),
|
| 982 |
+
)
|
| 983 |
+
else:
|
| 984 |
+
an_input = prompt_session.prompt(
|
| 985 |
+
f"{get_flair_and_username()} {self.PATH} $ ",
|
| 986 |
+
completer=self.completer,
|
| 987 |
+
search_ignore_case=True,
|
| 988 |
+
)
|
| 989 |
+
# Get input from user without auto-completion
|
| 990 |
+
else:
|
| 991 |
+
an_input = input(f"{get_flair_and_username()} {self.PATH} $ ")
|
| 992 |
+
|
| 993 |
+
except (KeyboardInterrupt, EOFError):
|
| 994 |
+
# Exit in case of keyboard interrupt
|
| 995 |
+
an_input = "exit"
|
| 996 |
+
|
| 997 |
+
try:
|
| 998 |
+
# Allow user to go back to root
|
| 999 |
+
an_input = "home" if an_input == "/" else an_input
|
| 1000 |
+
|
| 1001 |
+
# Process the input command
|
| 1002 |
+
self.queue = self.switch(an_input)
|
| 1003 |
+
|
| 1004 |
+
except SystemExit:
|
| 1005 |
+
session.console.print(
|
| 1006 |
+
f"[red]The command '{an_input}' doesn't exist on the {self.PATH} menu.[/red]\n",
|
| 1007 |
+
)
|
| 1008 |
+
similar_cmd = difflib.get_close_matches(
|
| 1009 |
+
an_input.split(" ")[0] if " " in an_input else an_input,
|
| 1010 |
+
self.controller_choices,
|
| 1011 |
+
n=1,
|
| 1012 |
+
cutoff=0.7,
|
| 1013 |
+
)
|
| 1014 |
+
if similar_cmd:
|
| 1015 |
+
if " " in an_input:
|
| 1016 |
+
candidate_input = (
|
| 1017 |
+
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
|
| 1018 |
+
)
|
| 1019 |
+
if candidate_input == an_input:
|
| 1020 |
+
an_input = ""
|
| 1021 |
+
self.queue = []
|
| 1022 |
+
session.console.print("\n")
|
| 1023 |
+
continue
|
| 1024 |
+
|
| 1025 |
+
an_input = candidate_input
|
| 1026 |
+
else:
|
| 1027 |
+
an_input = similar_cmd[0]
|
| 1028 |
+
|
| 1029 |
+
session.console.print(
|
| 1030 |
+
f"[green]Replacing by '{an_input}'.[/green]\n"
|
| 1031 |
+
)
|
| 1032 |
+
self.queue.insert(0, an_input)
|
cli/openbb_cli/controllers/base_platform_controller.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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__( # pylint: disable=too-many-positional-arguments
|
| 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 |
+
# Generate choices by combining indexed and key-based choices
|
| 79 |
+
action.choices = [
|
| 80 |
+
"OBB" + str(i)
|
| 81 |
+
for i in range(len(session.obbject_registry.obbjects))
|
| 82 |
+
] + [
|
| 83 |
+
obbject.extra["register_key"]
|
| 84 |
+
for obbject in session.obbject_registry.obbjects
|
| 85 |
+
if "register_key" in obbject.extra
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
action.type = str
|
| 89 |
+
action.nargs = None
|
| 90 |
+
|
| 91 |
+
def _intersect_data_processing_commands(self, ns_parser):
|
| 92 |
+
"""Intersect data processing commands and change the obbject id into an actual obbject."""
|
| 93 |
+
if hasattr(ns_parser, "data"):
|
| 94 |
+
if "OBB" in ns_parser.data:
|
| 95 |
+
ns_parser.data = int(ns_parser.data.replace("OBB", ""))
|
| 96 |
+
|
| 97 |
+
if (ns_parser.data in range(len(session.obbject_registry.obbjects))) or (
|
| 98 |
+
ns_parser.data in session.obbject_registry.obbject_keys
|
| 99 |
+
):
|
| 100 |
+
obbject = session.obbject_registry.get(ns_parser.data)
|
| 101 |
+
if obbject and isinstance(obbject, OBBject):
|
| 102 |
+
setattr(ns_parser, "data", obbject.results)
|
| 103 |
+
|
| 104 |
+
return ns_parser
|
| 105 |
+
|
| 106 |
+
def _generate_sub_controllers(self):
|
| 107 |
+
"""Handle paths."""
|
| 108 |
+
for path, value in self.paths.items():
|
| 109 |
+
if value == "path":
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
sub_menu_translators = {}
|
| 113 |
+
choices_commands = []
|
| 114 |
+
|
| 115 |
+
for translator_name, translator in self.translators.items():
|
| 116 |
+
if f"{self._name}_{path}" in translator_name:
|
| 117 |
+
new_name = translator_name.replace(f"{self._name}_{path}_", "")
|
| 118 |
+
sub_menu_translators[new_name] = translator
|
| 119 |
+
choices_commands.append(new_name)
|
| 120 |
+
|
| 121 |
+
if translator_name in self.CHOICES_COMMANDS:
|
| 122 |
+
self.CHOICES_COMMANDS.remove(translator_name)
|
| 123 |
+
|
| 124 |
+
# Create the sub controller as a new class
|
| 125 |
+
class_name = f"{self._name.capitalize()}{path.capitalize()}Controller"
|
| 126 |
+
SubController = type(
|
| 127 |
+
class_name,
|
| 128 |
+
(PlatformController,),
|
| 129 |
+
{
|
| 130 |
+
"CHOICES_GENERATION": True,
|
| 131 |
+
# "CHOICES_MENUS": [],
|
| 132 |
+
"CHOICES_COMMANDS": choices_commands,
|
| 133 |
+
},
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
self._generate_controller_call(
|
| 137 |
+
controller=SubController,
|
| 138 |
+
name=path,
|
| 139 |
+
parent_path=self.path,
|
| 140 |
+
translators=sub_menu_translators,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
def _generate_commands(self):
|
| 144 |
+
"""Generate commands."""
|
| 145 |
+
for name, translator in self.translators.items():
|
| 146 |
+
# Prepare the translator name to create a command call in the controller
|
| 147 |
+
new_name = name.replace(f"{self._name}_", "")
|
| 148 |
+
|
| 149 |
+
self._generate_command_call(name=new_name, translator=translator)
|
| 150 |
+
|
| 151 |
+
def _generate_command_call(self, name, translator):
|
| 152 |
+
"""Generate command call."""
|
| 153 |
+
|
| 154 |
+
def method(self, other_args: List[str], translator=translator):
|
| 155 |
+
"""Call the translator."""
|
| 156 |
+
parser = translator.parser
|
| 157 |
+
|
| 158 |
+
if ns_parser := self.parse_known_args_and_warn(
|
| 159 |
+
parser=parser,
|
| 160 |
+
other_args=other_args,
|
| 161 |
+
export_allowed="raw_data_and_figures",
|
| 162 |
+
):
|
| 163 |
+
try:
|
| 164 |
+
ns_parser = self._intersect_data_processing_commands(ns_parser)
|
| 165 |
+
export = hasattr(ns_parser, "export") and ns_parser.export
|
| 166 |
+
store_obbject = (
|
| 167 |
+
hasattr(ns_parser, "register_obbject")
|
| 168 |
+
and ns_parser.register_obbject
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
obbject = translator.execute_func(parsed_args=ns_parser)
|
| 172 |
+
df: pd.DataFrame = pd.DataFrame()
|
| 173 |
+
fig: Optional[OpenBBFigure] = None
|
| 174 |
+
title = f"{self.PATH}{translator.func.__name__}"
|
| 175 |
+
|
| 176 |
+
if obbject:
|
| 177 |
+
if isinstance(obbject, list):
|
| 178 |
+
obbject = OBBject(results=obbject)
|
| 179 |
+
|
| 180 |
+
if isinstance(obbject, OBBject):
|
| 181 |
+
if (
|
| 182 |
+
session.max_obbjects_exceeded()
|
| 183 |
+
and obbject.results
|
| 184 |
+
and store_obbject
|
| 185 |
+
):
|
| 186 |
+
session.obbject_registry.remove()
|
| 187 |
+
session.console.print(
|
| 188 |
+
"[yellow]Maximum number of OBBjects reached. The oldest entry was removed.[yellow]"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# use the obbject to store the command so we can display it later on results
|
| 192 |
+
obbject.extra["command"] = f"{title} {' '.join(other_args)}"
|
| 193 |
+
# if there is a registry key in the parser, store to the obbject
|
| 194 |
+
if (
|
| 195 |
+
hasattr(ns_parser, "register_key")
|
| 196 |
+
and ns_parser.register_key
|
| 197 |
+
):
|
| 198 |
+
if (
|
| 199 |
+
ns_parser.register_key
|
| 200 |
+
not in session.obbject_registry.obbject_keys
|
| 201 |
+
):
|
| 202 |
+
obbject.extra["register_key"] = str(
|
| 203 |
+
ns_parser.register_key
|
| 204 |
+
)
|
| 205 |
+
else:
|
| 206 |
+
session.console.print(
|
| 207 |
+
f"[yellow]Key `{ns_parser.register_key}` already exists in the registry."
|
| 208 |
+
"The `OBBject` was kept without the key.[/yellow]"
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
if store_obbject:
|
| 212 |
+
# store the obbject in the registry
|
| 213 |
+
register_result = session.obbject_registry.register(
|
| 214 |
+
obbject
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
# we need to force to re-link so that the new obbject
|
| 218 |
+
# is immediately available for data processing commands
|
| 219 |
+
self._link_obbject_to_data_processing_commands()
|
| 220 |
+
# also update the completer
|
| 221 |
+
self.update_completer(self.choices_default)
|
| 222 |
+
|
| 223 |
+
if (
|
| 224 |
+
session.settings.SHOW_MSG_OBBJECT_REGISTRY
|
| 225 |
+
and register_result
|
| 226 |
+
):
|
| 227 |
+
session.console.print(
|
| 228 |
+
"Added `OBBject` to cached results."
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
# making the dataframe available either for printing or exporting
|
| 232 |
+
df = obbject.to_dataframe()
|
| 233 |
+
|
| 234 |
+
if hasattr(ns_parser, "chart") and ns_parser.chart:
|
| 235 |
+
fig = obbject.chart.fig if obbject.chart else None
|
| 236 |
+
if not export:
|
| 237 |
+
obbject.show()
|
| 238 |
+
elif session.settings.USE_INTERACTIVE_DF and not export:
|
| 239 |
+
obbject.charting.table()
|
| 240 |
+
else:
|
| 241 |
+
if isinstance(df.columns, pd.RangeIndex):
|
| 242 |
+
df.columns = [str(i) for i in df.columns]
|
| 243 |
+
|
| 244 |
+
print_rich_table(
|
| 245 |
+
df=df, show_index=True, title=title, export=export
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
elif isinstance(obbject, dict):
|
| 249 |
+
df = pd.DataFrame.from_dict(obbject, orient="columns")
|
| 250 |
+
print_rich_table(
|
| 251 |
+
df=df, show_index=True, title=title, export=export
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
elif not isinstance(obbject, OBBject):
|
| 255 |
+
session.console.print(obbject)
|
| 256 |
+
|
| 257 |
+
if export and not df.empty:
|
| 258 |
+
sheet_name = getattr(ns_parser, "sheet_name", None)
|
| 259 |
+
if sheet_name and isinstance(sheet_name, list):
|
| 260 |
+
sheet_name = sheet_name[0]
|
| 261 |
+
|
| 262 |
+
export_data(
|
| 263 |
+
export_type=",".join(ns_parser.export),
|
| 264 |
+
dir_path=os.path.dirname(os.path.abspath(__file__)),
|
| 265 |
+
func_name=translator.func.__name__,
|
| 266 |
+
df=df,
|
| 267 |
+
sheet_name=sheet_name,
|
| 268 |
+
figure=fig,
|
| 269 |
+
)
|
| 270 |
+
elif export and df.empty:
|
| 271 |
+
session.console.print("[yellow]No data to export.[/yellow]")
|
| 272 |
+
|
| 273 |
+
except Exception as e:
|
| 274 |
+
session.console.print(f"[red]{e}[/]\n")
|
| 275 |
+
return
|
| 276 |
+
|
| 277 |
+
# Bind the method to the class
|
| 278 |
+
bound_method = MethodType(method, self)
|
| 279 |
+
|
| 280 |
+
# Update the wrapper and set the attribute
|
| 281 |
+
bound_method = update_wrapper( # type: ignore
|
| 282 |
+
partial(bound_method, translator=translator), method
|
| 283 |
+
)
|
| 284 |
+
setattr(self, f"call_{name}", bound_method)
|
| 285 |
+
|
| 286 |
+
def _generate_controller_call(self, controller, name, parent_path, translators):
|
| 287 |
+
"""Generate controller call."""
|
| 288 |
+
|
| 289 |
+
def method(self, _, controller, name, parent_path, translators):
|
| 290 |
+
"""Call the controller."""
|
| 291 |
+
self.queue = self.load_class(
|
| 292 |
+
class_ins=controller,
|
| 293 |
+
name=name,
|
| 294 |
+
parent_path=parent_path,
|
| 295 |
+
translators=translators,
|
| 296 |
+
queue=self.queue,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
# Bind the method to the class
|
| 300 |
+
bound_method = MethodType(method, self)
|
| 301 |
+
|
| 302 |
+
# Update the wrapper and set the attribute
|
| 303 |
+
bound_method = update_wrapper( # type: ignore
|
| 304 |
+
partial(
|
| 305 |
+
bound_method,
|
| 306 |
+
name=name,
|
| 307 |
+
parent_path=parent_path,
|
| 308 |
+
translators=translators,
|
| 309 |
+
controller=controller,
|
| 310 |
+
),
|
| 311 |
+
method,
|
| 312 |
+
)
|
| 313 |
+
setattr(self, f"call_{name}", bound_method)
|
| 314 |
+
|
| 315 |
+
def _get_command_description(self, command: str) -> str:
|
| 316 |
+
"""Get command description."""
|
| 317 |
+
command_description = (
|
| 318 |
+
obb.reference["paths"] # type: ignore
|
| 319 |
+
.get(f"{self.PATH}{command}", {})
|
| 320 |
+
.get("description", "")
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
if not command_description:
|
| 324 |
+
trl = self.translators.get(
|
| 325 |
+
f"{self._name}_{command}"
|
| 326 |
+
) or self.translators.get(command)
|
| 327 |
+
if trl and hasattr(trl, "parser"):
|
| 328 |
+
command_description = trl.parser.description
|
| 329 |
+
|
| 330 |
+
return command_description.split(".")[0].lower()
|
| 331 |
+
|
| 332 |
+
def _get_menu_description(self, menu: str) -> str:
|
| 333 |
+
"""Get menu description."""
|
| 334 |
+
|
| 335 |
+
def _get_sub_menu_commands():
|
| 336 |
+
"""Get sub menu commands."""
|
| 337 |
+
sub_path = f"{self.PATH[1:].replace('/','_')}{menu}"
|
| 338 |
+
commands = []
|
| 339 |
+
for trl in self.translators:
|
| 340 |
+
if sub_path in trl:
|
| 341 |
+
commands.append(trl.replace(f"{sub_path}_", ""))
|
| 342 |
+
return commands
|
| 343 |
+
|
| 344 |
+
menu_description = (
|
| 345 |
+
obb.reference["routers"] # type: ignore
|
| 346 |
+
.get(f"{self.PATH}{menu}", {})
|
| 347 |
+
.get("description", "")
|
| 348 |
+
) or ""
|
| 349 |
+
if menu_description:
|
| 350 |
+
return menu_description.split(".")[0].lower()
|
| 351 |
+
|
| 352 |
+
# If no description is found, return the sub menu commands
|
| 353 |
+
return ", ".join(_get_sub_menu_commands())
|
| 354 |
+
|
| 355 |
+
def print_help(self):
|
| 356 |
+
"""Print help."""
|
| 357 |
+
mt = MenuText(self.PATH)
|
| 358 |
+
|
| 359 |
+
if self.CHOICES_MENUS:
|
| 360 |
+
for menu in self.CHOICES_MENUS:
|
| 361 |
+
description = self._get_menu_description(menu)
|
| 362 |
+
mt.add_menu(name=menu, description=description)
|
| 363 |
+
|
| 364 |
+
if self.CHOICES_COMMANDS:
|
| 365 |
+
mt.add_raw("\n")
|
| 366 |
+
|
| 367 |
+
if self.CHOICES_COMMANDS:
|
| 368 |
+
for command in self.CHOICES_COMMANDS:
|
| 369 |
+
command_description = self._get_command_description(command)
|
| 370 |
+
mt.add_cmd(
|
| 371 |
+
name=command.replace(f"{self._name}_", ""),
|
| 372 |
+
description=command_description,
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
if session.obbject_registry.obbjects:
|
| 376 |
+
mt.add_info("\nCached Results")
|
| 377 |
+
for key, value in list(session.obbject_registry.all.items())[
|
| 378 |
+
: session.settings.N_TO_DISPLAY_OBBJECT_REGISTRY
|
| 379 |
+
]:
|
| 380 |
+
mt.add_raw(
|
| 381 |
+
f"[yellow]OBB{key}[/yellow]: {value['command']}",
|
| 382 |
+
left_spacing=True,
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
session.console.print(text=mt.menu_text, menu=self.PATH)
|
| 386 |
+
|
| 387 |
+
if mt.warnings:
|
| 388 |
+
session.console.print("")
|
| 389 |
+
for w in mt.warnings:
|
| 390 |
+
w_str = str(w).replace("{", "").replace("}", "").replace("'", "")
|
| 391 |
+
session.console.print(f"[yellow]{w_str}[/yellow]")
|
| 392 |
+
session.console.print("")
|
cli/openbb_cli/controllers/choices.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, Tuple
|
| 8 |
+
from unittest.mock import patch
|
| 9 |
+
|
| 10 |
+
from openbb_cli.controllers.utils import (
|
| 11 |
+
check_file_type_saved,
|
| 12 |
+
check_positive,
|
| 13 |
+
validate_register_key,
|
| 14 |
+
)
|
| 15 |
+
from openbb_cli.session import Session
|
| 16 |
+
|
| 17 |
+
session = Session()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def __mock_parse_known_args_and_warn(
|
| 21 |
+
controller, # pylint: disable=unused-argument
|
| 22 |
+
parser: ArgumentParser,
|
| 23 |
+
other_args: List[str],
|
| 24 |
+
export_allowed: Literal[
|
| 25 |
+
"no_export", "raw_data_only", "figures_only", "raw_data_and_figures"
|
| 26 |
+
] = "no_export",
|
| 27 |
+
raw: bool = False,
|
| 28 |
+
limit: int = 0,
|
| 29 |
+
) -> None:
|
| 30 |
+
"""Add arguments.
|
| 31 |
+
|
| 32 |
+
Add the arguments that would have normally added by :
|
| 33 |
+
- openbb_cli.base_controller.BaseController.parse_known_args_and_warn
|
| 34 |
+
|
| 35 |
+
Parameters
|
| 36 |
+
----------
|
| 37 |
+
parser: argparse.ArgumentParser
|
| 38 |
+
Parser with predefined arguments
|
| 39 |
+
other_args: List[str]
|
| 40 |
+
list of arguments to parse
|
| 41 |
+
export_allowed: Literal["no_export", "raw_data_only", "figures_only", "raw_data_and_figures"]
|
| 42 |
+
Export options
|
| 43 |
+
raw: bool
|
| 44 |
+
Add the --raw flag
|
| 45 |
+
limit: int
|
| 46 |
+
Add a --limit flag with this number default
|
| 47 |
+
"""
|
| 48 |
+
_ = other_args
|
| 49 |
+
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"-h", "--help", action="store_true", help="show this help message"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
if export_allowed != "no_export":
|
| 55 |
+
choices_export = []
|
| 56 |
+
help_export = "Does not export!"
|
| 57 |
+
|
| 58 |
+
if export_allowed == "raw_data_only":
|
| 59 |
+
choices_export = ["csv", "json", "xlsx"]
|
| 60 |
+
help_export = "Export raw data into csv, json or xlsx."
|
| 61 |
+
elif export_allowed == "figures_only":
|
| 62 |
+
choices_export = ["png", "jpg"]
|
| 63 |
+
help_export = "Export figure into png or jpg."
|
| 64 |
+
else:
|
| 65 |
+
choices_export = ["csv", "json", "xlsx", "png", "jpg"]
|
| 66 |
+
help_export = (
|
| 67 |
+
"Export raw data into csv, json, xlsx and figure into png or jpg."
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--export",
|
| 72 |
+
default="",
|
| 73 |
+
type=check_file_type_saved(choices_export),
|
| 74 |
+
dest="export",
|
| 75 |
+
help=help_export,
|
| 76 |
+
choices=choices_export,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
if raw:
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--raw",
|
| 82 |
+
dest="raw",
|
| 83 |
+
action="store_true",
|
| 84 |
+
default=False,
|
| 85 |
+
help="Flag to display raw data",
|
| 86 |
+
)
|
| 87 |
+
if limit > 0:
|
| 88 |
+
parser.add_argument(
|
| 89 |
+
"-l",
|
| 90 |
+
"--limit",
|
| 91 |
+
dest="limit",
|
| 92 |
+
default=limit,
|
| 93 |
+
help="Number of entries to show in data.",
|
| 94 |
+
type=check_positive,
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
parser.add_argument(
|
| 98 |
+
"--register_obbject",
|
| 99 |
+
dest="register_obbject",
|
| 100 |
+
action="store_false",
|
| 101 |
+
default=True,
|
| 102 |
+
help="Flag to store data in the OBBject registry, True by default.",
|
| 103 |
+
)
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
"--register_key",
|
| 106 |
+
dest="register_key",
|
| 107 |
+
default="",
|
| 108 |
+
help="Key to reference data in the OBBject registry.",
|
| 109 |
+
type=validate_register_key,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> Tuple:
|
| 114 |
+
"""Add arguments.
|
| 115 |
+
|
| 116 |
+
Add the arguments that would have normally added by:
|
| 117 |
+
- openbb_cli.parent_classes.BaseController.parse_simple_args
|
| 118 |
+
|
| 119 |
+
Parameters
|
| 120 |
+
----------
|
| 121 |
+
parser: argparse.ArgumentParser
|
| 122 |
+
Parser with predefined arguments
|
| 123 |
+
other_args: List[str]
|
| 124 |
+
List of arguments to parse
|
| 125 |
+
"""
|
| 126 |
+
parser.add_argument(
|
| 127 |
+
"-h", "--help", action="store_true", help="show this help message"
|
| 128 |
+
)
|
| 129 |
+
_ = other_args
|
| 130 |
+
return None, None
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def __get_command_func(controller, command: str):
|
| 134 |
+
"""Get the function with the name `f"call_{command}"` from controller object.
|
| 135 |
+
|
| 136 |
+
Parameters
|
| 137 |
+
----------
|
| 138 |
+
controller: BaseController
|
| 139 |
+
Instance of the CLI Controller.
|
| 140 |
+
command: str
|
| 141 |
+
A name from controller.CHOICES_COMMANDS
|
| 142 |
+
|
| 143 |
+
Returns
|
| 144 |
+
-------
|
| 145 |
+
Callable: Command function.
|
| 146 |
+
"""
|
| 147 |
+
if command not in controller.CHOICES_COMMANDS:
|
| 148 |
+
raise AttributeError(
|
| 149 |
+
f"The following command is not inside `CHOICES_COMMANDS` : '{command}'"
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
command = f"call_{command}"
|
| 153 |
+
command_func = getattr(controller, command)
|
| 154 |
+
command_func = unwrap(func=command_func)
|
| 155 |
+
|
| 156 |
+
if isfunction(command_func):
|
| 157 |
+
command_func = MethodType(command_func, controller)
|
| 158 |
+
|
| 159 |
+
return command_func
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def contains_functions_to_patch(command_func: Callable) -> bool:
|
| 163 |
+
"""Check command function.
|
| 164 |
+
|
| 165 |
+
Check if a `command_func` actually contains the functions we want to mock, i.e.:
|
| 166 |
+
- parse_simple_args
|
| 167 |
+
- parse_known_args_and_warn
|
| 168 |
+
|
| 169 |
+
Parameters
|
| 170 |
+
----------
|
| 171 |
+
command_func: Callable
|
| 172 |
+
Function to check.
|
| 173 |
+
|
| 174 |
+
Returns
|
| 175 |
+
-------
|
| 176 |
+
bool: Whether or not `command_func` contains the mocked functions.
|
| 177 |
+
"""
|
| 178 |
+
co_names = command_func.__code__.co_names
|
| 179 |
+
|
| 180 |
+
return bool(
|
| 181 |
+
"parse_simple_args" in co_names or "parse_known_args_and_warn" in co_names
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@contextmanager
|
| 186 |
+
def __patch_controller_functions(controller):
|
| 187 |
+
"""Patch controller functions.
|
| 188 |
+
|
| 189 |
+
Patch the following function from a BaseController instance:
|
| 190 |
+
- parse_simple_args
|
| 191 |
+
- parse_known_args_and_warn
|
| 192 |
+
|
| 193 |
+
These functions take an 'argparse.ArgumentParser' object as parameter.
|
| 194 |
+
We want to intercept this 'argparse.ArgumentParser' object.
|
| 195 |
+
|
| 196 |
+
Parameters
|
| 197 |
+
----------
|
| 198 |
+
controller: BaseController
|
| 199 |
+
BaseController object that needs to be patched.
|
| 200 |
+
|
| 201 |
+
Returns
|
| 202 |
+
-------
|
| 203 |
+
List[Callable]: List of mocked functions.
|
| 204 |
+
"""
|
| 205 |
+
bound_mock_parse_known_args_and_warn = MethodType(
|
| 206 |
+
__mock_parse_known_args_and_warn,
|
| 207 |
+
controller,
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
rich = patch(
|
| 211 |
+
target="openbb_cli.config.console.Console.print",
|
| 212 |
+
return_value=None,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
patcher_list = [
|
| 216 |
+
patch.object(
|
| 217 |
+
target=controller,
|
| 218 |
+
attribute="parse_simple_args",
|
| 219 |
+
side_effect=__mock_parse_simple_args,
|
| 220 |
+
return_value=(None, None),
|
| 221 |
+
),
|
| 222 |
+
patch.object(
|
| 223 |
+
target=controller,
|
| 224 |
+
attribute="parse_known_args_and_warn",
|
| 225 |
+
side_effect=bound_mock_parse_known_args_and_warn,
|
| 226 |
+
return_value=None,
|
| 227 |
+
),
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
if not session.settings.DEBUG_MODE:
|
| 231 |
+
rich.start()
|
| 232 |
+
patched_function_list = []
|
| 233 |
+
for patcher in patcher_list:
|
| 234 |
+
patched_function_list.append(patcher.start())
|
| 235 |
+
|
| 236 |
+
yield patched_function_list
|
| 237 |
+
|
| 238 |
+
if not session.settings.DEBUG_MODE:
|
| 239 |
+
rich.stop()
|
| 240 |
+
for patcher in patcher_list:
|
| 241 |
+
patcher.stop()
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _get_argument_parser(
|
| 245 |
+
controller,
|
| 246 |
+
command: str,
|
| 247 |
+
) -> ArgumentParser:
|
| 248 |
+
"""Intercept the ArgumentParser instance from the command function.
|
| 249 |
+
|
| 250 |
+
A command function being a function starting with `call_`, like:
|
| 251 |
+
- call_help
|
| 252 |
+
- call_overview
|
| 253 |
+
- call_load
|
| 254 |
+
|
| 255 |
+
Parameters
|
| 256 |
+
----------
|
| 257 |
+
controller: BaseController
|
| 258 |
+
Instance of the CLI Controller.
|
| 259 |
+
command: str
|
| 260 |
+
A name from `controller.CHOICES_COMMANDS`.
|
| 261 |
+
|
| 262 |
+
Returns
|
| 263 |
+
-------
|
| 264 |
+
ArgumentParser: ArgumentParser instance from the command function.
|
| 265 |
+
"""
|
| 266 |
+
command_func: Callable = __get_command_func(controller=controller, command=command)
|
| 267 |
+
|
| 268 |
+
if not contains_functions_to_patch(command_func=command_func):
|
| 269 |
+
raise AssertionError(
|
| 270 |
+
f"One of these functions should be inside `call_{command}`:\n"
|
| 271 |
+
" - parse_simple_args\n"
|
| 272 |
+
" - parse_known_args_and_warn\n"
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
with __patch_controller_functions(controller=controller) as patched_function_list:
|
| 276 |
+
command_func([])
|
| 277 |
+
|
| 278 |
+
call_count = 0
|
| 279 |
+
for patched_function in patched_function_list:
|
| 280 |
+
call_count += patched_function.call_count
|
| 281 |
+
if patched_function.call_count == 1:
|
| 282 |
+
args, kwargs = patched_function.call_args
|
| 283 |
+
argument_parser = (
|
| 284 |
+
kwargs["parser"] if kwargs.get("parser", None) else args[0]
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
if call_count != 1:
|
| 288 |
+
raise AssertionError(
|
| 289 |
+
f"One of these functions should be called once inside `call_{command}`:\n"
|
| 290 |
+
" - parse_simple_args\n"
|
| 291 |
+
" - parse_known_args_and_warn\n"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# pylint: disable=possibly-used-before-assignment
|
| 295 |
+
return argument_parser
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def _build_command_choice_map(argument_parser: ArgumentParser) -> dict:
|
| 299 |
+
"""Build the choice map for a command."""
|
| 300 |
+
choice_map: dict = {}
|
| 301 |
+
for action in argument_parser._actions: # pylint: disable=protected-access
|
| 302 |
+
if action.help == SUPPRESS:
|
| 303 |
+
continue
|
| 304 |
+
if len(action.option_strings) == 1:
|
| 305 |
+
long_name = action.option_strings[0]
|
| 306 |
+
short_name = ""
|
| 307 |
+
elif len(action.option_strings) == 2:
|
| 308 |
+
short_name = action.option_strings[0]
|
| 309 |
+
long_name = action.option_strings[1]
|
| 310 |
+
else:
|
| 311 |
+
raise AttributeError(f"Invalid argument_parser: {argument_parser}")
|
| 312 |
+
|
| 313 |
+
if hasattr(action, "choices") and action.choices:
|
| 314 |
+
choice_map[long_name] = {str(c): {} for c in action.choices}
|
| 315 |
+
else:
|
| 316 |
+
choice_map[long_name] = {}
|
| 317 |
+
|
| 318 |
+
if short_name and long_name:
|
| 319 |
+
choice_map[short_name] = long_name
|
| 320 |
+
|
| 321 |
+
return choice_map
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def build_controller_choice_map(controller) -> dict:
|
| 325 |
+
"""Build the choice map for a controller."""
|
| 326 |
+
command_list = controller.CHOICES_COMMANDS
|
| 327 |
+
controller_choice_map: dict = {c: {} for c in controller.controller_choices}
|
| 328 |
+
|
| 329 |
+
for command in command_list:
|
| 330 |
+
try:
|
| 331 |
+
argument_parser = _get_argument_parser(
|
| 332 |
+
controller=controller,
|
| 333 |
+
command=command,
|
| 334 |
+
)
|
| 335 |
+
controller_choice_map[command] = _build_command_choice_map(
|
| 336 |
+
argument_parser=argument_parser
|
| 337 |
+
)
|
| 338 |
+
except Exception as exception:
|
| 339 |
+
if session.settings.DEBUG_MODE:
|
| 340 |
+
raise Exception(
|
| 341 |
+
f"On command : `{command}`.\n{str(exception)}"
|
| 342 |
+
) from exception
|
| 343 |
+
|
| 344 |
+
return controller_choice_map
|
cli/openbb_cli/controllers/cli_controller.py
ADDED
|
@@ -0,0 +1,944 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Main CLI Module."""
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import contextlib
|
| 6 |
+
import difflib
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
import webbrowser
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from functools import partial, update_wrapper
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from types import MethodType
|
| 16 |
+
from typing import Any, Dict, List, Optional
|
| 17 |
+
|
| 18 |
+
import pandas as pd
|
| 19 |
+
import requests
|
| 20 |
+
from openbb import obb
|
| 21 |
+
from openbb_cli.config import constants
|
| 22 |
+
from openbb_cli.config.constants import (
|
| 23 |
+
ASSETS_DIRECTORY,
|
| 24 |
+
ENV_FILE_SETTINGS,
|
| 25 |
+
HOME_DIRECTORY,
|
| 26 |
+
REPOSITORY_DIRECTORY,
|
| 27 |
+
)
|
| 28 |
+
from openbb_cli.config.menu_text import MenuText
|
| 29 |
+
from openbb_cli.controllers.base_controller import BaseController
|
| 30 |
+
from openbb_cli.controllers.platform_controller_factory import (
|
| 31 |
+
PlatformControllerFactory,
|
| 32 |
+
)
|
| 33 |
+
from openbb_cli.controllers.script_parser import is_reset, parse_openbb_script
|
| 34 |
+
from openbb_cli.controllers.utils import (
|
| 35 |
+
bootup,
|
| 36 |
+
first_time_user,
|
| 37 |
+
get_flair_and_username,
|
| 38 |
+
parse_and_split_input,
|
| 39 |
+
print_goodbye,
|
| 40 |
+
print_rich_table,
|
| 41 |
+
reset,
|
| 42 |
+
suppress_stdout,
|
| 43 |
+
welcome_message,
|
| 44 |
+
)
|
| 45 |
+
from openbb_cli.session import Session
|
| 46 |
+
from prompt_toolkit.formatted_text import HTML
|
| 47 |
+
from prompt_toolkit.styles import Style
|
| 48 |
+
from pydantic import BaseModel
|
| 49 |
+
|
| 50 |
+
PLATFORM_ROUTERS = {
|
| 51 |
+
d: "menu" if not isinstance(getattr(obb, d), BaseModel) else "command"
|
| 52 |
+
for d in dir(obb)
|
| 53 |
+
if "_" not in d
|
| 54 |
+
}
|
| 55 |
+
NON_DATA_ROUTERS = ["coverage", "account", "reference", "system", "user"]
|
| 56 |
+
DATA_PROCESSING_ROUTERS = ["technical", "quantitative", "econometrics"]
|
| 57 |
+
|
| 58 |
+
# pylint: disable=too-many-public-methods,import-outside-toplevel, too-many-function-args
|
| 59 |
+
# pylint: disable=too-many-branches,no-member,C0302,too-many-return-statements, inconsistent-return-statements
|
| 60 |
+
|
| 61 |
+
env_file = str(ENV_FILE_SETTINGS)
|
| 62 |
+
session = Session()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class CLIController(BaseController):
|
| 66 |
+
"""CLI Controller class."""
|
| 67 |
+
|
| 68 |
+
CHOICES_COMMANDS = ["record", "stop", "exe", "results"]
|
| 69 |
+
CHOICES_MENUS = [
|
| 70 |
+
"settings",
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
for router, value in PLATFORM_ROUTERS.items():
|
| 74 |
+
if value == "menu":
|
| 75 |
+
CHOICES_MENUS.append(router)
|
| 76 |
+
else:
|
| 77 |
+
CHOICES_COMMANDS.append(router)
|
| 78 |
+
|
| 79 |
+
PATH = "/"
|
| 80 |
+
CHOICES_GENERATION = False
|
| 81 |
+
|
| 82 |
+
def __init__(self, jobs_cmds: Optional[List[str]] = None):
|
| 83 |
+
"""Construct CLI controller."""
|
| 84 |
+
self.ROUTINE_FILES: Dict[str, str] = dict()
|
| 85 |
+
self.ROUTINE_DEFAULT_FILES: Dict[str, str] = dict()
|
| 86 |
+
self.ROUTINE_PERSONAL_FILES: Dict[str, str] = dict()
|
| 87 |
+
self.ROUTINE_CHOICES: Dict[str, Any] = dict()
|
| 88 |
+
|
| 89 |
+
super().__init__(jobs_cmds)
|
| 90 |
+
|
| 91 |
+
self.queue: List[str] = list()
|
| 92 |
+
|
| 93 |
+
if jobs_cmds:
|
| 94 |
+
self.queue = parse_and_split_input(
|
| 95 |
+
an_input=" ".join(jobs_cmds), custom_filters=[]
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
self.update_success = False
|
| 99 |
+
|
| 100 |
+
self._generate_platform_commands()
|
| 101 |
+
|
| 102 |
+
self.update_runtime_choices()
|
| 103 |
+
|
| 104 |
+
def _generate_platform_commands(self):
|
| 105 |
+
"""Generate Platform based commands/menus."""
|
| 106 |
+
|
| 107 |
+
def method_call_class(self, _, controller, name, parent_path, target):
|
| 108 |
+
self.queue = self.load_class(
|
| 109 |
+
controller, name, parent_path, target, self.queue
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# pylint: disable=unused-argument
|
| 113 |
+
def method_call_command(self, _, router: str):
|
| 114 |
+
"""Call command."""
|
| 115 |
+
mdl = getattr(obb, router)
|
| 116 |
+
df = pd.DataFrame.from_dict(mdl.model_dump(), orient="index")
|
| 117 |
+
if isinstance(df.columns, pd.RangeIndex):
|
| 118 |
+
df.columns = [str(i) for i in df.columns]
|
| 119 |
+
return print_rich_table(df, show_index=True)
|
| 120 |
+
|
| 121 |
+
for router, value in PLATFORM_ROUTERS.items():
|
| 122 |
+
target = getattr(obb, router)
|
| 123 |
+
|
| 124 |
+
if value == "menu":
|
| 125 |
+
pcf = PlatformControllerFactory(
|
| 126 |
+
target, reference=obb.reference["paths"] # type: ignore
|
| 127 |
+
)
|
| 128 |
+
DynamicController = pcf.create()
|
| 129 |
+
|
| 130 |
+
# Bind the method to the class
|
| 131 |
+
bound_method = MethodType(method_call_class, self)
|
| 132 |
+
|
| 133 |
+
# Update the wrapper and set the attribute
|
| 134 |
+
bound_method = update_wrapper( # type: ignore
|
| 135 |
+
partial(
|
| 136 |
+
bound_method,
|
| 137 |
+
controller=DynamicController,
|
| 138 |
+
name=router,
|
| 139 |
+
target=target,
|
| 140 |
+
parent_path=self.path,
|
| 141 |
+
),
|
| 142 |
+
method_call_class,
|
| 143 |
+
)
|
| 144 |
+
else:
|
| 145 |
+
bound_method = MethodType(method_call_command, self)
|
| 146 |
+
bound_method = update_wrapper( # type: ignore
|
| 147 |
+
partial(bound_method, router=router),
|
| 148 |
+
method_call_command,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
setattr(self, f"call_{router}", bound_method)
|
| 152 |
+
|
| 153 |
+
def update_runtime_choices(self):
|
| 154 |
+
"""Update runtime choices."""
|
| 155 |
+
routines_directory = Path(session.user.preferences.export_directory, "routines")
|
| 156 |
+
|
| 157 |
+
if session.prompt_session and session.settings.USE_PROMPT_TOOLKIT:
|
| 158 |
+
# choices: dict = self.choices_default
|
| 159 |
+
choices: dict = {c: {} for c in self.controller_choices} # type: ignore
|
| 160 |
+
|
| 161 |
+
self.ROUTINE_FILES = {
|
| 162 |
+
filepath.name: filepath # type: ignore
|
| 163 |
+
for filepath in routines_directory.rglob("*.openbb")
|
| 164 |
+
}
|
| 165 |
+
self.ROUTINE_DEFAULT_FILES = {
|
| 166 |
+
filepath.name: filepath # type: ignore
|
| 167 |
+
for filepath in Path(routines_directory / "hub" / "default").rglob(
|
| 168 |
+
"*.openbb"
|
| 169 |
+
)
|
| 170 |
+
}
|
| 171 |
+
self.ROUTINE_PERSONAL_FILES = {
|
| 172 |
+
filepath.name: filepath # type: ignore
|
| 173 |
+
for filepath in Path(routines_directory / "hub" / "personal").rglob(
|
| 174 |
+
"*.openbb"
|
| 175 |
+
)
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
choices["exe"] = {
|
| 179 |
+
"--file": {
|
| 180 |
+
filename: {} for filename in list(self.ROUTINE_FILES.keys())
|
| 181 |
+
},
|
| 182 |
+
"-f": "--file",
|
| 183 |
+
"--example": None,
|
| 184 |
+
"-e": "--example",
|
| 185 |
+
"--input": None,
|
| 186 |
+
"-i": "--input",
|
| 187 |
+
"--url": None,
|
| 188 |
+
"--help": None,
|
| 189 |
+
"-h": "--help",
|
| 190 |
+
}
|
| 191 |
+
choices["record"] = {
|
| 192 |
+
"--name": None,
|
| 193 |
+
"-n": "--name",
|
| 194 |
+
"--description": None,
|
| 195 |
+
"-d": "--description",
|
| 196 |
+
"--public": None,
|
| 197 |
+
"-p": "--public",
|
| 198 |
+
"--tag1": {c: None for c in constants.SCRIPT_TAGS},
|
| 199 |
+
"--tag2": {c: None for c in constants.SCRIPT_TAGS},
|
| 200 |
+
"--tag3": {c: None for c in constants.SCRIPT_TAGS},
|
| 201 |
+
"--help": None,
|
| 202 |
+
"-h": "--help",
|
| 203 |
+
}
|
| 204 |
+
choices["stop"] = {"--help": None, "-h": "--help"}
|
| 205 |
+
choices["results"] = {
|
| 206 |
+
"--help": None,
|
| 207 |
+
"-h": "--help",
|
| 208 |
+
"--export": {c: None for c in ["csv", "json", "xlsx", "png", "jpg"]},
|
| 209 |
+
"--index": None,
|
| 210 |
+
"--key": None,
|
| 211 |
+
"--chart": None,
|
| 212 |
+
"--sheet_name": None,
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
self.update_completer(choices)
|
| 216 |
+
|
| 217 |
+
def print_help(self):
|
| 218 |
+
"""Print help."""
|
| 219 |
+
mt = MenuText("")
|
| 220 |
+
|
| 221 |
+
mt.add_info("Configure the platform and manage your account")
|
| 222 |
+
for router, value in PLATFORM_ROUTERS.items():
|
| 223 |
+
if router not in NON_DATA_ROUTERS or router in ["reference", "coverage"]:
|
| 224 |
+
continue
|
| 225 |
+
if value == "menu":
|
| 226 |
+
menu_description = (
|
| 227 |
+
obb.reference["routers"] # type: ignore
|
| 228 |
+
.get(f"{self.PATH}{router}", {})
|
| 229 |
+
.get("description")
|
| 230 |
+
) or ""
|
| 231 |
+
mt.add_menu(
|
| 232 |
+
name=router,
|
| 233 |
+
description=menu_description.split(".")[0].lower(),
|
| 234 |
+
)
|
| 235 |
+
else:
|
| 236 |
+
mt.add_cmd(router)
|
| 237 |
+
|
| 238 |
+
mt.add_info("\nConfigure your CLI")
|
| 239 |
+
mt.add_menu(
|
| 240 |
+
"settings",
|
| 241 |
+
description="enable and disable feature flags, preferences and settings",
|
| 242 |
+
)
|
| 243 |
+
mt.add_raw("\n")
|
| 244 |
+
mt.add_info("Record and execute your own .openbb routine scripts")
|
| 245 |
+
mt.add_cmd("record", description="start recording current session")
|
| 246 |
+
mt.add_cmd(
|
| 247 |
+
"stop", description="stop session recording and convert to .openbb routine"
|
| 248 |
+
)
|
| 249 |
+
mt.add_cmd(
|
| 250 |
+
"exe",
|
| 251 |
+
description="execute .openbb routine scripts (use exe --example for an example)",
|
| 252 |
+
)
|
| 253 |
+
mt.add_raw("\n")
|
| 254 |
+
mt.add_info("Retrieve data from different asset classes and providers")
|
| 255 |
+
|
| 256 |
+
for router, value in PLATFORM_ROUTERS.items():
|
| 257 |
+
if router in NON_DATA_ROUTERS or router in DATA_PROCESSING_ROUTERS:
|
| 258 |
+
continue
|
| 259 |
+
if value == "menu":
|
| 260 |
+
menu_description = (
|
| 261 |
+
obb.reference["routers"] # type: ignore
|
| 262 |
+
.get(f"{self.PATH}{router}", {})
|
| 263 |
+
.get("description")
|
| 264 |
+
) or ""
|
| 265 |
+
mt.add_menu(
|
| 266 |
+
name=router,
|
| 267 |
+
description=menu_description.split(".")[0].lower(),
|
| 268 |
+
)
|
| 269 |
+
else:
|
| 270 |
+
mt.add_cmd(router)
|
| 271 |
+
|
| 272 |
+
if any(router in PLATFORM_ROUTERS for router in DATA_PROCESSING_ROUTERS):
|
| 273 |
+
mt.add_info("\nAnalyze and process previously obtained data")
|
| 274 |
+
|
| 275 |
+
for router, value in PLATFORM_ROUTERS.items():
|
| 276 |
+
if router not in DATA_PROCESSING_ROUTERS:
|
| 277 |
+
continue
|
| 278 |
+
if value == "menu":
|
| 279 |
+
menu_description = (
|
| 280 |
+
obb.reference["routers"] # type: ignore
|
| 281 |
+
.get(f"{self.PATH}{router}", {})
|
| 282 |
+
.get("description")
|
| 283 |
+
) or ""
|
| 284 |
+
mt.add_menu(
|
| 285 |
+
name=router,
|
| 286 |
+
description=menu_description.split(".")[0].lower(),
|
| 287 |
+
)
|
| 288 |
+
else:
|
| 289 |
+
mt.add_cmd(router)
|
| 290 |
+
|
| 291 |
+
mt.add_raw("\n")
|
| 292 |
+
mt.add_cmd("results")
|
| 293 |
+
if session.obbject_registry.obbjects:
|
| 294 |
+
mt.add_info("\nCached Results")
|
| 295 |
+
for key, value in list(session.obbject_registry.all.items())[ # type: ignore
|
| 296 |
+
: session.settings.N_TO_DISPLAY_OBBJECT_REGISTRY
|
| 297 |
+
]:
|
| 298 |
+
mt.add_raw(
|
| 299 |
+
f"[yellow]OBB{key}[/yellow]: {value['command']}", # type: ignore[index]
|
| 300 |
+
left_spacing=True,
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
session.console.print(text=mt.menu_text, menu="Home")
|
| 304 |
+
self.update_runtime_choices()
|
| 305 |
+
|
| 306 |
+
def parse_input(self, an_input: str) -> List:
|
| 307 |
+
"""Overwrite the BaseController parse_input for `askobb` and 'exe'.
|
| 308 |
+
|
| 309 |
+
This will allow us to search for something like "P/E" ratio.
|
| 310 |
+
"""
|
| 311 |
+
# Filtering out sorting parameters with forward slashes like P/E
|
| 312 |
+
sort_filter = r"((\ -q |\ --question|\ ).*?(/))"
|
| 313 |
+
# Filter out urls
|
| 314 |
+
url = r"(exe (--url )?(https?://)?my\.openbb\.(dev|co)/u/.*/routine/.*)"
|
| 315 |
+
custom_filters = [sort_filter, url]
|
| 316 |
+
return parse_and_split_input(an_input=an_input, custom_filters=custom_filters)
|
| 317 |
+
|
| 318 |
+
def call_settings(self, _):
|
| 319 |
+
"""Process settings command."""
|
| 320 |
+
from openbb_cli.controllers.settings_controller import (
|
| 321 |
+
SettingsController,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
self.queue = self.load_class(SettingsController, self.queue)
|
| 325 |
+
|
| 326 |
+
def call_exe(self, other_args: List[str]):
|
| 327 |
+
"""Process exe command."""
|
| 328 |
+
# Merge rest of string path to other_args and remove queue since it is a dir
|
| 329 |
+
other_args += self.queue
|
| 330 |
+
|
| 331 |
+
if not other_args:
|
| 332 |
+
session.console.print(
|
| 333 |
+
"[info]Provide a path to the routine you wish to execute. For an example, please use "
|
| 334 |
+
"`exe --example`.\n[/info]"
|
| 335 |
+
)
|
| 336 |
+
return
|
| 337 |
+
parser = argparse.ArgumentParser(
|
| 338 |
+
add_help=False,
|
| 339 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 340 |
+
prog="exe",
|
| 341 |
+
description="Execute automated routine script. For an example, please use "
|
| 342 |
+
"`exe --example`.",
|
| 343 |
+
)
|
| 344 |
+
parser.add_argument(
|
| 345 |
+
"--file",
|
| 346 |
+
"-f",
|
| 347 |
+
help="The path or .openbb file to run.",
|
| 348 |
+
dest="file",
|
| 349 |
+
required="-h" not in other_args
|
| 350 |
+
and "--help" not in other_args
|
| 351 |
+
and "-e" not in other_args
|
| 352 |
+
and "--example" not in other_args
|
| 353 |
+
and "--url" not in other_args
|
| 354 |
+
and "my.openbb" not in other_args[0],
|
| 355 |
+
type=str,
|
| 356 |
+
nargs="+",
|
| 357 |
+
)
|
| 358 |
+
parser.add_argument(
|
| 359 |
+
"-i",
|
| 360 |
+
"--input",
|
| 361 |
+
help="Select multiple inputs to be replaced in the routine and separated by commas. E.g. GME,AMC,BTC-USD",
|
| 362 |
+
dest="routine_args",
|
| 363 |
+
type=str,
|
| 364 |
+
)
|
| 365 |
+
parser.add_argument(
|
| 366 |
+
"-e",
|
| 367 |
+
"--example",
|
| 368 |
+
help="Run an example script to understand how routines can be used.",
|
| 369 |
+
dest="example",
|
| 370 |
+
action="store_true",
|
| 371 |
+
default=False,
|
| 372 |
+
)
|
| 373 |
+
parser.add_argument(
|
| 374 |
+
"--url", help="URL to run openbb script from.", dest="url", type=str
|
| 375 |
+
)
|
| 376 |
+
if other_args and "-" not in other_args[0][0]:
|
| 377 |
+
if other_args[0].startswith("my.") or other_args[0].startswith("http"):
|
| 378 |
+
other_args.insert(0, "--url")
|
| 379 |
+
else:
|
| 380 |
+
other_args.insert(0, "--file")
|
| 381 |
+
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
| 382 |
+
if ns_parser:
|
| 383 |
+
if ns_parser.example:
|
| 384 |
+
routine_path = ASSETS_DIRECTORY / "routines" / "routine_example.openbb"
|
| 385 |
+
session.console.print( # TODO: Point to docs when ready
|
| 386 |
+
"[info]Executing an example, please visit our docs "
|
| 387 |
+
"to learn how to create your own script.[/info]\n"
|
| 388 |
+
)
|
| 389 |
+
time.sleep(3)
|
| 390 |
+
elif ns_parser.url:
|
| 391 |
+
if not ns_parser.url.startswith(
|
| 392 |
+
"https"
|
| 393 |
+
) and not ns_parser.url.startswith("http:"):
|
| 394 |
+
url = "https://" + ns_parser.url
|
| 395 |
+
elif ns_parser.url.startswith("http://"):
|
| 396 |
+
url = ns_parser.url.replace("http://", "https://")
|
| 397 |
+
else:
|
| 398 |
+
url = ns_parser.url
|
| 399 |
+
username = url.split("/")[-3]
|
| 400 |
+
script_name = url.split("/")[-1]
|
| 401 |
+
file_name = f"{username}_{script_name}.openbb"
|
| 402 |
+
final_url = f"{url}?raw=true"
|
| 403 |
+
response = requests.get(final_url, timeout=10)
|
| 404 |
+
if response.status_code != 200:
|
| 405 |
+
session.console.print(
|
| 406 |
+
"[red]Could not find the requested script.[/red]"
|
| 407 |
+
)
|
| 408 |
+
return
|
| 409 |
+
routine_text = response.json()["script"]
|
| 410 |
+
file_path = Path(session.user.preferences.export_directory, "routines")
|
| 411 |
+
routine_path = file_path / file_name
|
| 412 |
+
with open(routine_path, "w") as file:
|
| 413 |
+
file.write(routine_text)
|
| 414 |
+
self.update_runtime_choices()
|
| 415 |
+
|
| 416 |
+
elif ns_parser.file:
|
| 417 |
+
file_path = " ".join(ns_parser.file) # type: ignore
|
| 418 |
+
# if string is not in this format "default/file.openbb" then check for files in ROUTINE_FILES
|
| 419 |
+
full_path = file_path
|
| 420 |
+
hub_routine = file_path.split("/") # type: ignore
|
| 421 |
+
# Change with: my.openbb.co
|
| 422 |
+
if hub_routine[0] == "default":
|
| 423 |
+
routine_path = Path(
|
| 424 |
+
self.ROUTINE_DEFAULT_FILES.get(hub_routine[1], full_path)
|
| 425 |
+
)
|
| 426 |
+
elif hub_routine[0] == "personal":
|
| 427 |
+
routine_path = Path(
|
| 428 |
+
self.ROUTINE_PERSONAL_FILES.get(hub_routine[1], full_path)
|
| 429 |
+
)
|
| 430 |
+
else:
|
| 431 |
+
routine_path = Path(self.ROUTINE_FILES.get(file_path, full_path)) # type: ignore
|
| 432 |
+
else:
|
| 433 |
+
return
|
| 434 |
+
|
| 435 |
+
try:
|
| 436 |
+
with open(routine_path) as fp:
|
| 437 |
+
raw_lines = list(fp)
|
| 438 |
+
|
| 439 |
+
script_inputs = []
|
| 440 |
+
# Capture ARGV either as list if args separated by commas or as single value
|
| 441 |
+
if routine_args := ns_parser.routine_args:
|
| 442 |
+
pattern = r"\[(.*?)\]"
|
| 443 |
+
matches = re.findall(pattern, routine_args)
|
| 444 |
+
|
| 445 |
+
for match in matches:
|
| 446 |
+
routine_args = routine_args.replace(f"[{match}]", "")
|
| 447 |
+
script_inputs.append(match)
|
| 448 |
+
|
| 449 |
+
script_inputs.extend(
|
| 450 |
+
[val for val in routine_args.split(",") if val]
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
err, parsed_script = parse_openbb_script(
|
| 454 |
+
raw_lines=raw_lines, script_inputs=script_inputs
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
# If there err output is not an empty string then it means there was an
|
| 458 |
+
# issue in parsing the routine and therefore we don't want to feed it
|
| 459 |
+
# to the terminal
|
| 460 |
+
if err:
|
| 461 |
+
session.console.print(err)
|
| 462 |
+
return
|
| 463 |
+
|
| 464 |
+
self.queue = [
|
| 465 |
+
val
|
| 466 |
+
for val in parse_and_split_input(
|
| 467 |
+
an_input=parsed_script, custom_filters=[]
|
| 468 |
+
)
|
| 469 |
+
if val
|
| 470 |
+
]
|
| 471 |
+
|
| 472 |
+
if "export" in self.queue[0]:
|
| 473 |
+
export_path = self.queue[0].split(" ")[1]
|
| 474 |
+
# If the path selected does not start from the user root, give relative location from root
|
| 475 |
+
if export_path[0] == "~":
|
| 476 |
+
export_path = export_path.replace(
|
| 477 |
+
"~", HOME_DIRECTORY.as_posix()
|
| 478 |
+
)
|
| 479 |
+
elif export_path[0] != "/":
|
| 480 |
+
export_path = os.path.join(
|
| 481 |
+
os.path.dirname(os.path.abspath(__file__)), export_path
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
# Check if the directory exists
|
| 485 |
+
if os.path.isdir(export_path):
|
| 486 |
+
session.console.print(
|
| 487 |
+
f"Export data to be saved in the selected folder: '{export_path}'"
|
| 488 |
+
)
|
| 489 |
+
else:
|
| 490 |
+
os.makedirs(export_path)
|
| 491 |
+
session.console.print(
|
| 492 |
+
f"[green]Folder '{export_path}' successfully created.[/green]"
|
| 493 |
+
)
|
| 494 |
+
self.queue = self.queue[1:]
|
| 495 |
+
|
| 496 |
+
except FileNotFoundError:
|
| 497 |
+
session.console.print(
|
| 498 |
+
f"[red]File '{routine_path}' doesn't exist.[/red]"
|
| 499 |
+
)
|
| 500 |
+
return
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def handle_job_cmds(jobs_cmds: Optional[List[str]]) -> Optional[List[str]]:
|
| 504 |
+
"""Handle job commands."""
|
| 505 |
+
export_path = ""
|
| 506 |
+
if jobs_cmds and "export" in jobs_cmds[0]:
|
| 507 |
+
commands = jobs_cmds[0].split("/")
|
| 508 |
+
first_split = commands[0].split(" ")
|
| 509 |
+
if len(first_split) > 1:
|
| 510 |
+
export_path = first_split[1]
|
| 511 |
+
jobs_cmds = ["/".join(commands[1:])]
|
| 512 |
+
if not export_path:
|
| 513 |
+
return jobs_cmds
|
| 514 |
+
if export_path[0] == "~":
|
| 515 |
+
export_path = export_path.replace("~", HOME_DIRECTORY.as_posix())
|
| 516 |
+
elif export_path[0] != "/":
|
| 517 |
+
export_path = os.path.join(
|
| 518 |
+
os.path.dirname(os.path.abspath(__file__)), export_path
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
# Check if the directory exists
|
| 522 |
+
if os.path.isdir(export_path):
|
| 523 |
+
session.console.print(
|
| 524 |
+
f"Export data to be saved in the selected folder: '{export_path}'"
|
| 525 |
+
)
|
| 526 |
+
else:
|
| 527 |
+
os.makedirs(export_path)
|
| 528 |
+
session.console.print(
|
| 529 |
+
f"[green]Folder '{export_path}' successfully created.[/green]"
|
| 530 |
+
)
|
| 531 |
+
return jobs_cmds
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
# pylint: disable=unused-argument
|
| 535 |
+
def run_cli(jobs_cmds: Optional[List[str]] = None, test_mode=False):
|
| 536 |
+
"""Run the CLI menu."""
|
| 537 |
+
ret_code = 1
|
| 538 |
+
t_controller = CLIController(jobs_cmds)
|
| 539 |
+
an_input = ""
|
| 540 |
+
|
| 541 |
+
jobs_cmds = handle_job_cmds(jobs_cmds)
|
| 542 |
+
|
| 543 |
+
bootup()
|
| 544 |
+
if not jobs_cmds:
|
| 545 |
+
welcome_message()
|
| 546 |
+
|
| 547 |
+
if first_time_user():
|
| 548 |
+
with contextlib.suppress(EOFError):
|
| 549 |
+
webbrowser.open("https://docs.openbb.co/cli")
|
| 550 |
+
|
| 551 |
+
t_controller.print_help()
|
| 552 |
+
|
| 553 |
+
while ret_code:
|
| 554 |
+
# There is a command in the queue
|
| 555 |
+
if t_controller.queue and len(t_controller.queue) > 0:
|
| 556 |
+
# If the command is quitting the menu we want to return in here
|
| 557 |
+
if t_controller.queue[0] in ("q", "..", "quit"):
|
| 558 |
+
print_goodbye()
|
| 559 |
+
break
|
| 560 |
+
|
| 561 |
+
# Consume 1 element from the queue
|
| 562 |
+
an_input = t_controller.queue[0]
|
| 563 |
+
t_controller.queue = t_controller.queue[1:]
|
| 564 |
+
|
| 565 |
+
# Print the current location because this was an instruction and we want user to know what was the action
|
| 566 |
+
if an_input and an_input.split(" ")[0] in t_controller.CHOICES_COMMANDS:
|
| 567 |
+
session.console.print(f"{get_flair_and_username()} / $ {an_input}")
|
| 568 |
+
|
| 569 |
+
# Get input command from user
|
| 570 |
+
else:
|
| 571 |
+
try:
|
| 572 |
+
# Get input from user using auto-completion
|
| 573 |
+
if session.prompt_session and session.settings.USE_PROMPT_TOOLKIT:
|
| 574 |
+
# Check if toolbar hint was enabled
|
| 575 |
+
if session.settings.TOOLBAR_HINT:
|
| 576 |
+
an_input = session.prompt_session.prompt( # type: ignore[union-attr]
|
| 577 |
+
f"{get_flair_and_username()} / $ ",
|
| 578 |
+
completer=t_controller.completer,
|
| 579 |
+
search_ignore_case=True,
|
| 580 |
+
bottom_toolbar=HTML(
|
| 581 |
+
'<style bg="ansiblack" fg="ansiwhite">[h]</style> help menu '
|
| 582 |
+
'<style bg="ansiblack" fg="ansiwhite">[q]</style> return to previous menu '
|
| 583 |
+
'<style bg="ansiblack" fg="ansiwhite">[e]</style> exit the program '
|
| 584 |
+
'<style bg="ansiblack" fg="ansiwhite">[cmd -h]</style> '
|
| 585 |
+
"see usage and available options "
|
| 586 |
+
),
|
| 587 |
+
style=Style.from_dict(
|
| 588 |
+
{
|
| 589 |
+
"bottom-toolbar": "#ffffff bg:#333333",
|
| 590 |
+
}
|
| 591 |
+
),
|
| 592 |
+
)
|
| 593 |
+
else:
|
| 594 |
+
an_input = session.prompt_session.prompt( # type: ignore[union-attr]
|
| 595 |
+
f"{get_flair_and_username()} / $ ",
|
| 596 |
+
completer=t_controller.completer,
|
| 597 |
+
search_ignore_case=True,
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
# Get input from user without auto-completion
|
| 601 |
+
else:
|
| 602 |
+
an_input = input(f"{get_flair_and_username()} / $ ")
|
| 603 |
+
|
| 604 |
+
except (KeyboardInterrupt, EOFError):
|
| 605 |
+
print_goodbye()
|
| 606 |
+
break
|
| 607 |
+
|
| 608 |
+
try:
|
| 609 |
+
# Process the input command
|
| 610 |
+
t_controller.queue = t_controller.switch(an_input)
|
| 611 |
+
|
| 612 |
+
if an_input in ("q", "quit", "..", "exit", "e"):
|
| 613 |
+
print_goodbye()
|
| 614 |
+
break
|
| 615 |
+
|
| 616 |
+
# Check if the user wants to reset application
|
| 617 |
+
if an_input in ("r", "reset") or t_controller.update_success:
|
| 618 |
+
reset(t_controller.queue if t_controller.queue else [])
|
| 619 |
+
break
|
| 620 |
+
|
| 621 |
+
except SystemExit:
|
| 622 |
+
session.console.print(
|
| 623 |
+
f"[red]The command '{an_input}' doesn't exist on the / menu.[/red]\n",
|
| 624 |
+
)
|
| 625 |
+
similar_cmd = difflib.get_close_matches(
|
| 626 |
+
an_input.split(" ")[0] if " " in an_input else an_input,
|
| 627 |
+
t_controller.controller_choices,
|
| 628 |
+
n=1,
|
| 629 |
+
cutoff=0.7,
|
| 630 |
+
)
|
| 631 |
+
if similar_cmd:
|
| 632 |
+
an_input = similar_cmd[0]
|
| 633 |
+
if " " in an_input:
|
| 634 |
+
candidate_input = (
|
| 635 |
+
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
|
| 636 |
+
)
|
| 637 |
+
if candidate_input == an_input:
|
| 638 |
+
an_input = ""
|
| 639 |
+
t_controller.queue = []
|
| 640 |
+
session.console.print("\n")
|
| 641 |
+
continue
|
| 642 |
+
an_input = candidate_input
|
| 643 |
+
|
| 644 |
+
session.console.print(f"[green]Replacing by '{an_input}'.[/green]")
|
| 645 |
+
t_controller.queue.insert(0, an_input)
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def insert_start_slash(cmds: List[str]) -> List[str]:
|
| 649 |
+
"""Insert a slash at the beginning of a command sequence."""
|
| 650 |
+
if not cmds[0].startswith("/"):
|
| 651 |
+
cmds[0] = f"/{cmds[0]}"
|
| 652 |
+
if cmds[0].startswith("/home"):
|
| 653 |
+
cmds[0] = f"/{cmds[0][5:]}"
|
| 654 |
+
return cmds
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
def run_scripts(
|
| 658 |
+
path: Path,
|
| 659 |
+
test_mode: bool = False,
|
| 660 |
+
verbose: bool = False,
|
| 661 |
+
routines_args: Optional[List[str]] = None,
|
| 662 |
+
special_arguments: Optional[Dict[str, str]] = None,
|
| 663 |
+
output: bool = True,
|
| 664 |
+
):
|
| 665 |
+
"""Run given .openbb scripts.
|
| 666 |
+
|
| 667 |
+
Parameters
|
| 668 |
+
----------
|
| 669 |
+
path : str
|
| 670 |
+
The location of the .openbb file
|
| 671 |
+
test_mode : bool
|
| 672 |
+
Whether the CLI is in test mode
|
| 673 |
+
verbose : bool
|
| 674 |
+
Whether to run tests in verbose mode
|
| 675 |
+
routines_args : List[str]
|
| 676 |
+
One or multiple inputs to be replaced in the routine and separated by commas.
|
| 677 |
+
E.g. GME,AMC,BTC-USD
|
| 678 |
+
special_arguments: Optional[Dict[str, str]]
|
| 679 |
+
Replace `${key=default}` with `value` for every key in the dictionary
|
| 680 |
+
output: bool
|
| 681 |
+
Whether to log tests to txt files
|
| 682 |
+
"""
|
| 683 |
+
if not path.exists():
|
| 684 |
+
session.console.print(f"File '{path}' doesn't exist. Launching base CLI.\n")
|
| 685 |
+
if not test_mode:
|
| 686 |
+
run_cli()
|
| 687 |
+
|
| 688 |
+
# THIS NEEDS TO BE REFACTORED!!! - ITS USED FOR TESTING
|
| 689 |
+
with path.open() as fp:
|
| 690 |
+
raw_lines = [x for x in fp if (not is_reset(x)) and ("#" not in x) and x]
|
| 691 |
+
raw_lines = [
|
| 692 |
+
raw_line.strip("\n") for raw_line in raw_lines if raw_line.strip("\n")
|
| 693 |
+
]
|
| 694 |
+
|
| 695 |
+
if routines_args:
|
| 696 |
+
lines = []
|
| 697 |
+
for rawline in raw_lines:
|
| 698 |
+
templine = rawline
|
| 699 |
+
for i, arg in enumerate(routines_args):
|
| 700 |
+
templine = templine.replace(f"$ARGV[{i}]", arg)
|
| 701 |
+
lines.append(templine)
|
| 702 |
+
# Handle new testing arguments:
|
| 703 |
+
elif special_arguments:
|
| 704 |
+
lines = []
|
| 705 |
+
for line in raw_lines:
|
| 706 |
+
new_line = re.sub(
|
| 707 |
+
r"\${[^{]+=[^{]+}",
|
| 708 |
+
lambda x: replace_dynamic(x, special_arguments), # type: ignore
|
| 709 |
+
line,
|
| 710 |
+
)
|
| 711 |
+
lines.append(new_line)
|
| 712 |
+
|
| 713 |
+
else:
|
| 714 |
+
lines = raw_lines
|
| 715 |
+
|
| 716 |
+
if test_mode and "exit" not in lines[-1]:
|
| 717 |
+
lines.append("exit")
|
| 718 |
+
|
| 719 |
+
# Deals with the export with a path with "/" in it
|
| 720 |
+
export_folder = ""
|
| 721 |
+
if "export" in lines[0]:
|
| 722 |
+
export_folder = lines[0].split("export ")[1].rstrip()
|
| 723 |
+
lines = lines[1:]
|
| 724 |
+
|
| 725 |
+
simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}"
|
| 726 |
+
file_cmds = simulate_argv.replace("//", "/home/").split()
|
| 727 |
+
file_cmds = insert_start_slash(file_cmds) if file_cmds else file_cmds
|
| 728 |
+
file_cmds = (
|
| 729 |
+
[f"export {export_folder}{' '.join(file_cmds)}"]
|
| 730 |
+
if export_folder
|
| 731 |
+
else [" ".join(file_cmds)]
|
| 732 |
+
)
|
| 733 |
+
|
| 734 |
+
if not test_mode or verbose:
|
| 735 |
+
run_cli(file_cmds, test_mode=True)
|
| 736 |
+
else:
|
| 737 |
+
with suppress_stdout():
|
| 738 |
+
session.console.print(f"To ensure: {output}")
|
| 739 |
+
if output:
|
| 740 |
+
timestamp = datetime.now().timestamp()
|
| 741 |
+
stamp_str = str(timestamp).replace(".", "")
|
| 742 |
+
whole_path = Path(REPOSITORY_DIRECTORY / "integration_test_output")
|
| 743 |
+
whole_path.mkdir(parents=True, exist_ok=True)
|
| 744 |
+
first_cmd = file_cmds[0].split("/")[1]
|
| 745 |
+
with open(
|
| 746 |
+
whole_path / f"{stamp_str}_{first_cmd}_output.txt", "w"
|
| 747 |
+
) as output_file, contextlib.redirect_stdout(output_file):
|
| 748 |
+
run_cli(file_cmds, test_mode=True)
|
| 749 |
+
else:
|
| 750 |
+
run_cli(file_cmds, test_mode=True)
|
| 751 |
+
|
| 752 |
+
|
| 753 |
+
def replace_dynamic(match: re.Match, special_arguments: Dict[str, str]) -> str:
|
| 754 |
+
"""Replace ${key=default} with value in special_arguments if it exists, else with default.
|
| 755 |
+
|
| 756 |
+
Parameters
|
| 757 |
+
----------
|
| 758 |
+
match: re.Match[str]
|
| 759 |
+
The match object
|
| 760 |
+
special_arguments: Dict[str, str]
|
| 761 |
+
The key value pairs to replace in the scripts
|
| 762 |
+
|
| 763 |
+
Returns
|
| 764 |
+
----------
|
| 765 |
+
str
|
| 766 |
+
The new string
|
| 767 |
+
"""
|
| 768 |
+
cleaned = match[0].replace("{", "").replace("}", "").replace("$", "")
|
| 769 |
+
key, default = cleaned.split("=")
|
| 770 |
+
dict_value = special_arguments.get(key, default)
|
| 771 |
+
if dict_value:
|
| 772 |
+
return dict_value
|
| 773 |
+
return default
|
| 774 |
+
|
| 775 |
+
|
| 776 |
+
def run_routine(file: str, routines_args=Optional[str]):
|
| 777 |
+
"""Execute command routine from .openbb file."""
|
| 778 |
+
user_routine_path = Path(session.user.preferences.export_directory, "routines")
|
| 779 |
+
default_routine_path = ASSETS_DIRECTORY / "routines" / file
|
| 780 |
+
|
| 781 |
+
if user_routine_path.exists():
|
| 782 |
+
run_scripts(path=user_routine_path, routines_args=routines_args)
|
| 783 |
+
elif default_routine_path.exists():
|
| 784 |
+
run_scripts(path=default_routine_path, routines_args=routines_args)
|
| 785 |
+
else:
|
| 786 |
+
session.console.print(
|
| 787 |
+
f"Routine not found, please put your `.openbb` file into : {user_routine_path}."
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
# pylint: disable=unused-argument
|
| 792 |
+
def main(
|
| 793 |
+
debug: bool,
|
| 794 |
+
dev: bool,
|
| 795 |
+
path_list: List[str],
|
| 796 |
+
routines_args: Optional[List[str]] = None,
|
| 797 |
+
**kwargs,
|
| 798 |
+
):
|
| 799 |
+
"""Run the CLI with various options.
|
| 800 |
+
|
| 801 |
+
Parameters
|
| 802 |
+
----------
|
| 803 |
+
debug : bool
|
| 804 |
+
Whether to run the CLI in debug mode
|
| 805 |
+
dev:
|
| 806 |
+
Points backend towards development environment instead of production
|
| 807 |
+
test : bool
|
| 808 |
+
Whether to run the CLI in integrated test mode
|
| 809 |
+
filtert : str
|
| 810 |
+
Filter test files with given string in name
|
| 811 |
+
paths : List[str]
|
| 812 |
+
The paths to run for scripts or to test
|
| 813 |
+
verbose : bool
|
| 814 |
+
Whether to show output from tests
|
| 815 |
+
routines_args : List[str]
|
| 816 |
+
One or multiple inputs to be replaced in the routine and separated by commas.
|
| 817 |
+
E.g. GME,AMC,BTC-USD
|
| 818 |
+
"""
|
| 819 |
+
if debug:
|
| 820 |
+
session.settings.DEBUG_MODE = True
|
| 821 |
+
|
| 822 |
+
if dev:
|
| 823 |
+
session.settings.DEV_BACKEND = True
|
| 824 |
+
session.settings.BASE_URL = "https://payments.openbb.dev/"
|
| 825 |
+
session.settings.HUB_URL = "https://my.openbb.dev"
|
| 826 |
+
|
| 827 |
+
if isinstance(path_list, list) and path_list[0].endswith(".openbb"):
|
| 828 |
+
run_routine(file=path_list[0], routines_args=routines_args)
|
| 829 |
+
elif path_list:
|
| 830 |
+
argv_cmds = list([" ".join(path_list).replace(" /", "/home/")])
|
| 831 |
+
argv_cmds = insert_start_slash(argv_cmds) if argv_cmds else argv_cmds
|
| 832 |
+
run_cli(argv_cmds)
|
| 833 |
+
else:
|
| 834 |
+
run_cli()
|
| 835 |
+
|
| 836 |
+
|
| 837 |
+
def parse_args_and_run():
|
| 838 |
+
"""Parse input arguments and run CLI."""
|
| 839 |
+
parser = argparse.ArgumentParser(
|
| 840 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 841 |
+
prog="cli",
|
| 842 |
+
description="The OpenBB Platform CLI.",
|
| 843 |
+
)
|
| 844 |
+
parser.add_argument(
|
| 845 |
+
"-d",
|
| 846 |
+
"--debug",
|
| 847 |
+
dest="debug",
|
| 848 |
+
action="store_true",
|
| 849 |
+
default=False,
|
| 850 |
+
help="Runs the CLI in debug mode.",
|
| 851 |
+
)
|
| 852 |
+
parser.add_argument(
|
| 853 |
+
"--dev",
|
| 854 |
+
dest="dev",
|
| 855 |
+
action="store_true",
|
| 856 |
+
default=False,
|
| 857 |
+
help="Points backend towards development environment instead of production",
|
| 858 |
+
)
|
| 859 |
+
parser.add_argument(
|
| 860 |
+
"--file",
|
| 861 |
+
help="The path or .openbb file to run.",
|
| 862 |
+
dest="path",
|
| 863 |
+
nargs="+",
|
| 864 |
+
default="",
|
| 865 |
+
type=str,
|
| 866 |
+
)
|
| 867 |
+
parser.add_argument(
|
| 868 |
+
"-i",
|
| 869 |
+
"--input",
|
| 870 |
+
help=(
|
| 871 |
+
"Select multiple inputs to be replaced in the routine and separated by commas."
|
| 872 |
+
"E.g. GME,AMC,BTC-USD"
|
| 873 |
+
),
|
| 874 |
+
dest="routine_args",
|
| 875 |
+
type=lambda s: [str(item) for item in s.split(",")],
|
| 876 |
+
default=None,
|
| 877 |
+
)
|
| 878 |
+
parser.add_argument(
|
| 879 |
+
"-t",
|
| 880 |
+
"--test",
|
| 881 |
+
action="store_true",
|
| 882 |
+
help=(
|
| 883 |
+
"Run the CLI in testing mode. Also run this option and '-h'"
|
| 884 |
+
" to see testing argument options."
|
| 885 |
+
),
|
| 886 |
+
)
|
| 887 |
+
# The args -m, -f and --HistoryManager.hist_file are used only in reports menu
|
| 888 |
+
# by papermill and that's why they have suppress help.
|
| 889 |
+
parser.add_argument(
|
| 890 |
+
"-m",
|
| 891 |
+
help=argparse.SUPPRESS,
|
| 892 |
+
dest="module",
|
| 893 |
+
default="",
|
| 894 |
+
type=str,
|
| 895 |
+
)
|
| 896 |
+
parser.add_argument(
|
| 897 |
+
"-f",
|
| 898 |
+
help=argparse.SUPPRESS,
|
| 899 |
+
dest="module_file",
|
| 900 |
+
default="",
|
| 901 |
+
type=str,
|
| 902 |
+
)
|
| 903 |
+
parser.add_argument(
|
| 904 |
+
"--HistoryManager.hist_file",
|
| 905 |
+
help=argparse.SUPPRESS,
|
| 906 |
+
dest="module_hist_file",
|
| 907 |
+
default="",
|
| 908 |
+
type=str,
|
| 909 |
+
)
|
| 910 |
+
if sys.argv[1:] and "-" not in sys.argv[1][0]:
|
| 911 |
+
sys.argv.insert(1, "--file")
|
| 912 |
+
ns_parser, unknown = parser.parse_known_args()
|
| 913 |
+
|
| 914 |
+
# This ensures that if cli.py receives unknown args it will not start.
|
| 915 |
+
# Use -d flag if you want to see the unknown args.
|
| 916 |
+
if unknown:
|
| 917 |
+
if ns_parser.debug:
|
| 918 |
+
session.console.print(unknown)
|
| 919 |
+
else:
|
| 920 |
+
sys.exit(-1)
|
| 921 |
+
|
| 922 |
+
main(
|
| 923 |
+
ns_parser.debug,
|
| 924 |
+
ns_parser.dev,
|
| 925 |
+
ns_parser.path,
|
| 926 |
+
ns_parser.routine_args,
|
| 927 |
+
module=ns_parser.module,
|
| 928 |
+
module_file=ns_parser.module_file,
|
| 929 |
+
module_hist_file=ns_parser.module_hist_file,
|
| 930 |
+
)
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
def launch(
|
| 934 |
+
debug: bool = False, dev: bool = False, queue: Optional[List[str]] = None
|
| 935 |
+
) -> None:
|
| 936 |
+
"""Launch CLI."""
|
| 937 |
+
if queue:
|
| 938 |
+
main(debug, dev, queue, module="")
|
| 939 |
+
else:
|
| 940 |
+
parse_args_and_run()
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
if __name__ == "__main__":
|
| 944 |
+
parse_args_and_run()
|
cli/openbb_cli/controllers/hub_service.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Routines handler module."""
|
| 2 |
+
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
import requests
|
| 6 |
+
from openbb_cli.config.constants import (
|
| 7 |
+
CONNECTION_ERROR_MSG,
|
| 8 |
+
CONNECTION_TIMEOUT_MSG,
|
| 9 |
+
TIMEOUT,
|
| 10 |
+
)
|
| 11 |
+
from openbb_cli.session import Session
|
| 12 |
+
|
| 13 |
+
# created dictionaries for personal and default routines with the structure
|
| 14 |
+
# {"file_name" :["script","personal/default"]}
|
| 15 |
+
# and stored dictionaries in list
|
| 16 |
+
# created new directory structure to account for personal and default routines
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
session = Session()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# pylint: disable=too-many-arguments
|
| 23 |
+
def upload_routine(
|
| 24 |
+
auth_header: str,
|
| 25 |
+
name: str = "",
|
| 26 |
+
description: str = "",
|
| 27 |
+
routine: str = "",
|
| 28 |
+
override: bool = False,
|
| 29 |
+
tags: str = "",
|
| 30 |
+
public: bool = False,
|
| 31 |
+
timeout: int = TIMEOUT,
|
| 32 |
+
) -> Optional[requests.Response]:
|
| 33 |
+
"""Send a routine to the server.
|
| 34 |
+
|
| 35 |
+
Parameters
|
| 36 |
+
----------
|
| 37 |
+
auth_header : str
|
| 38 |
+
The authorization header, e.g. "Bearer <token>".
|
| 39 |
+
name : str
|
| 40 |
+
The name of the routine.
|
| 41 |
+
routine : str
|
| 42 |
+
The routine.
|
| 43 |
+
override : bool
|
| 44 |
+
Whether to override the routine if it already exists.
|
| 45 |
+
tags : str
|
| 46 |
+
The tags of the routine.
|
| 47 |
+
public : bool
|
| 48 |
+
Whether to make the routine public or not.
|
| 49 |
+
timeout : int
|
| 50 |
+
The timeout, by default TIMEOUT
|
| 51 |
+
|
| 52 |
+
Returns
|
| 53 |
+
-------
|
| 54 |
+
Optional[requests.Response]
|
| 55 |
+
The response from the post request.
|
| 56 |
+
"""
|
| 57 |
+
data = {
|
| 58 |
+
"name": name,
|
| 59 |
+
"description": description,
|
| 60 |
+
"script": routine,
|
| 61 |
+
"override": override,
|
| 62 |
+
"tags": tags,
|
| 63 |
+
"version": session.settings.VERSION,
|
| 64 |
+
"public": public,
|
| 65 |
+
}
|
| 66 |
+
_console = session.console
|
| 67 |
+
try:
|
| 68 |
+
response = requests.post(
|
| 69 |
+
headers={"Authorization": auth_header},
|
| 70 |
+
url=session.settings.BASE_URL + "/terminal/script",
|
| 71 |
+
json=data,
|
| 72 |
+
timeout=timeout,
|
| 73 |
+
)
|
| 74 |
+
if response.status_code == 200:
|
| 75 |
+
username = getattr(session.user.profile.hub_session, "username", None)
|
| 76 |
+
if not username:
|
| 77 |
+
_console.print("[red]No username found.[/red]")
|
| 78 |
+
_console.print("[red]Failed to upload your routine.[/red]")
|
| 79 |
+
return None
|
| 80 |
+
_console.print("[green]Successfully uploaded your routine.[/]")
|
| 81 |
+
|
| 82 |
+
hub_url = session.settings.HUB_URL
|
| 83 |
+
|
| 84 |
+
if public:
|
| 85 |
+
_console.print(
|
| 86 |
+
f"\n[yellow]Share or edit it at {hub_url}/u/{username}/routine/{name.replace(' ', '-')}[/]"
|
| 87 |
+
)
|
| 88 |
+
else:
|
| 89 |
+
_console.print(f"\n[yellow]Go to {hub_url} to edit this script,[/]")
|
| 90 |
+
_console.print(
|
| 91 |
+
f"[yellow]or even make it public so you can access it at "
|
| 92 |
+
f"{hub_url}/u/{username}/routine/{name.replace(' ', '-')}[/]"
|
| 93 |
+
)
|
| 94 |
+
elif response.status_code != 409: # 409: routine already exists
|
| 95 |
+
_console.print(
|
| 96 |
+
"[red]" + response.json().get("detail", "Unknown error.") + "[/red]"
|
| 97 |
+
)
|
| 98 |
+
return response
|
| 99 |
+
except requests.exceptions.ConnectionError:
|
| 100 |
+
_console.print(f"\n{CONNECTION_ERROR_MSG}")
|
| 101 |
+
return None
|
| 102 |
+
except requests.exceptions.Timeout:
|
| 103 |
+
_console.print(f"\n{CONNECTION_TIMEOUT_MSG}")
|
| 104 |
+
return None
|
| 105 |
+
except Exception:
|
| 106 |
+
_console.print("[red]Failed to upload your routine.[/red]")
|
| 107 |
+
return None
|
cli/openbb_cli/controllers/platform_controller_factory.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Platform controller factory to create a platform controller."""
|
| 2 |
+
|
| 3 |
+
from typing import Dict, List, Union
|
| 4 |
+
|
| 5 |
+
from openbb_cli.argparse_translator.argparse_class_processor import (
|
| 6 |
+
ArgparseClassProcessor,
|
| 7 |
+
)
|
| 8 |
+
from openbb_cli.controllers.base_platform_controller import PlatformController
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class PlatformControllerFactory:
|
| 12 |
+
"""Factory to create a platform controller."""
|
| 13 |
+
|
| 14 |
+
def __init__(self, platform_router: type, **kwargs):
|
| 15 |
+
"""Create the controller name."""
|
| 16 |
+
self.platform_router = platform_router
|
| 17 |
+
self._translated_target = ArgparseClassProcessor(
|
| 18 |
+
target_class=self.platform_router, reference=kwargs.get("reference", {})
|
| 19 |
+
)
|
| 20 |
+
self.router_name = (
|
| 21 |
+
str(type(self.platform_router))
|
| 22 |
+
.rsplit(".", maxsplit=1)[-1]
|
| 23 |
+
.replace("'>", "")
|
| 24 |
+
.replace("ROUTER_", "")
|
| 25 |
+
.lower()
|
| 26 |
+
)
|
| 27 |
+
self.controller_name = f"{self.router_name.capitalize()}Controller"
|
| 28 |
+
|
| 29 |
+
def create(self) -> type:
|
| 30 |
+
"""Create the platform controller."""
|
| 31 |
+
ClassName = self.controller_name
|
| 32 |
+
Parents = (PlatformController,)
|
| 33 |
+
Attributes: Dict[str, Union[bool, List[str]]] = {"CHOICES_GENERATION": True}
|
| 34 |
+
|
| 35 |
+
# Menu and Command choices generation
|
| 36 |
+
choices_menus: List[str] = []
|
| 37 |
+
choices_commands: List[str] = []
|
| 38 |
+
translators = self._translated_target.translators
|
| 39 |
+
paths = self._translated_target.paths
|
| 40 |
+
# menus
|
| 41 |
+
for key, value in paths.items():
|
| 42 |
+
if value == "path":
|
| 43 |
+
continue
|
| 44 |
+
choices_menus.append(key)
|
| 45 |
+
# commands
|
| 46 |
+
for name, _ in translators.items():
|
| 47 |
+
if any(f"{self.router_name}_{path}" in name for path in paths):
|
| 48 |
+
continue
|
| 49 |
+
new_name = name.replace(f"{self.router_name}_", "")
|
| 50 |
+
choices_commands.append(new_name)
|
| 51 |
+
|
| 52 |
+
Attributes["CHOICES_MENUS"] = choices_menus
|
| 53 |
+
Attributes["CHOICES_COMMANDS"] = choices_commands
|
| 54 |
+
|
| 55 |
+
# Use type to create the class
|
| 56 |
+
DynamicClass = type(ClassName, Parents, Attributes)
|
| 57 |
+
|
| 58 |
+
return DynamicClass
|
cli/openbb_cli/controllers/script_parser.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Routine functions for OpenBB Platform CLI."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from typing import Dict, List, Match, Optional, Tuple, Union
|
| 6 |
+
|
| 7 |
+
from dateutil.relativedelta import relativedelta
|
| 8 |
+
from openbb_cli.session import Session
|
| 9 |
+
|
| 10 |
+
session = Session()
|
| 11 |
+
|
| 12 |
+
# pylint: disable=too-many-statements,eval-used,consider-iterating-dictionary
|
| 13 |
+
# pylint: disable=too-many-branches,too-many-return-statements
|
| 14 |
+
|
| 15 |
+
# Necessary for OpenBB keywords
|
| 16 |
+
MONTHS_VALUE = {
|
| 17 |
+
"JANUARY": 1,
|
| 18 |
+
"FEBRUARY": 2,
|
| 19 |
+
"MARCH": 3,
|
| 20 |
+
"APRIL": 4,
|
| 21 |
+
"MAY": 5,
|
| 22 |
+
"JUNE": 6,
|
| 23 |
+
"JULY": 7,
|
| 24 |
+
"AUGUST": 8,
|
| 25 |
+
"SEPTEMBER": 9,
|
| 26 |
+
"OCTOBER": 10,
|
| 27 |
+
"NOVEMBER": 11,
|
| 28 |
+
"DECEMBER": 12,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
WEEKDAY_VALUE = {
|
| 32 |
+
"MONDAY": 0,
|
| 33 |
+
"TUESDAY": 1,
|
| 34 |
+
"WEDNESDAY": 2,
|
| 35 |
+
"THURSDAY": 3,
|
| 36 |
+
"FRIDAY": 4,
|
| 37 |
+
"SATURDAY": 5,
|
| 38 |
+
"SUNDAY": 6,
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def is_reset(command: str) -> bool:
|
| 43 |
+
"""Test whether a command is a reset command.
|
| 44 |
+
|
| 45 |
+
Parameters
|
| 46 |
+
----------
|
| 47 |
+
command : str
|
| 48 |
+
The command to test
|
| 49 |
+
|
| 50 |
+
Returns
|
| 51 |
+
-------
|
| 52 |
+
answer : bool
|
| 53 |
+
Whether the command is a reset command
|
| 54 |
+
"""
|
| 55 |
+
if "reset" in command:
|
| 56 |
+
return True
|
| 57 |
+
if command == "r":
|
| 58 |
+
return True
|
| 59 |
+
if command == "r\n":
|
| 60 |
+
return True
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def match_and_return_openbb_keyword_date(keyword: str) -> str: # noqa: PLR0911
|
| 65 |
+
"""Return OpenBB keyword into date.
|
| 66 |
+
|
| 67 |
+
Parameters
|
| 68 |
+
----------
|
| 69 |
+
keyword : str
|
| 70 |
+
String with potential OpenBB keyword (e.g. 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY)
|
| 71 |
+
|
| 72 |
+
Returns
|
| 73 |
+
----------
|
| 74 |
+
str: Date with format YYYY-MM-DD
|
| 75 |
+
"""
|
| 76 |
+
now = datetime.now()
|
| 77 |
+
for i, regex in enumerate([r"^\$(\d+)([A-Z]+)AGO$", r"^\$(\d+)([A-Z]+)FROMNOW$"]):
|
| 78 |
+
match = re.match(regex, keyword)
|
| 79 |
+
if match:
|
| 80 |
+
integer_value = int(match.group(1))
|
| 81 |
+
time_unit = match.group(2)
|
| 82 |
+
clean_time = time_unit.upper()
|
| 83 |
+
if "DAYS" in clean_time or "MONTHS" in clean_time or "YEARS" in clean_time:
|
| 84 |
+
kwargs = {time_unit.lower(): integer_value}
|
| 85 |
+
if i == 0:
|
| 86 |
+
return (now - relativedelta(**kwargs)).strftime("%Y-%m-%d") # type: ignore
|
| 87 |
+
return (now + relativedelta(**kwargs)).strftime("%Y-%m-%d") # type: ignore
|
| 88 |
+
|
| 89 |
+
match = re.search(r"\$LAST(\w+)", keyword)
|
| 90 |
+
if match:
|
| 91 |
+
time_unit = match.group(1)
|
| 92 |
+
# Check if it corresponds to a month
|
| 93 |
+
if time_unit in list(MONTHS_VALUE.keys()):
|
| 94 |
+
the_year = now.year
|
| 95 |
+
# Calculate the year and month for last month date
|
| 96 |
+
if now.month <= MONTHS_VALUE[time_unit]:
|
| 97 |
+
# If the current month is greater than the last date month, it means it is this year
|
| 98 |
+
the_year = now.year - 1
|
| 99 |
+
return datetime(the_year, MONTHS_VALUE[time_unit], 1).strftime("%Y-%m-%d")
|
| 100 |
+
|
| 101 |
+
# Check if it corresponds to a week day
|
| 102 |
+
if time_unit in list(WEEKDAY_VALUE.keys()):
|
| 103 |
+
if datetime.weekday(now) > WEEKDAY_VALUE[time_unit]:
|
| 104 |
+
return (
|
| 105 |
+
now
|
| 106 |
+
- timedelta(datetime.weekday(now))
|
| 107 |
+
+ timedelta(WEEKDAY_VALUE[time_unit])
|
| 108 |
+
).strftime("%Y-%m-%d")
|
| 109 |
+
return (
|
| 110 |
+
now
|
| 111 |
+
- timedelta(7)
|
| 112 |
+
- timedelta(datetime.weekday(now))
|
| 113 |
+
+ timedelta(WEEKDAY_VALUE[time_unit])
|
| 114 |
+
).strftime("%Y-%m-%d")
|
| 115 |
+
|
| 116 |
+
match = re.search(r"\$NEXT(\w+)", keyword)
|
| 117 |
+
if match:
|
| 118 |
+
time_unit = match.group(1)
|
| 119 |
+
# Check if it corresponds to a month
|
| 120 |
+
if time_unit in list(MONTHS_VALUE.keys()):
|
| 121 |
+
# Calculate the year and month for next month date
|
| 122 |
+
if now.month < MONTHS_VALUE[time_unit]:
|
| 123 |
+
# If the current month is greater than the last date month, it means it is this year
|
| 124 |
+
return datetime(now.year, MONTHS_VALUE[time_unit], 1).strftime(
|
| 125 |
+
"%Y-%m-%d"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
return datetime(now.year + 1, MONTHS_VALUE[time_unit], 1).strftime(
|
| 129 |
+
"%Y-%m-%d"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Check if it corresponds to a week day
|
| 133 |
+
if time_unit in list(WEEKDAY_VALUE.keys()):
|
| 134 |
+
if datetime.weekday(now) < WEEKDAY_VALUE[time_unit]:
|
| 135 |
+
return (
|
| 136 |
+
now
|
| 137 |
+
- timedelta(datetime.weekday(now))
|
| 138 |
+
+ timedelta(WEEKDAY_VALUE[time_unit])
|
| 139 |
+
).strftime("%Y-%m-%d")
|
| 140 |
+
return (
|
| 141 |
+
now
|
| 142 |
+
+ timedelta(7)
|
| 143 |
+
- timedelta(datetime.weekday(now))
|
| 144 |
+
+ timedelta(WEEKDAY_VALUE[time_unit])
|
| 145 |
+
).strftime("%Y-%m-%d")
|
| 146 |
+
|
| 147 |
+
return ""
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def parse_openbb_script( # noqa: PLR0911,PLR0912
|
| 151 |
+
raw_lines: List[str],
|
| 152 |
+
script_inputs: Optional[List[str]] = None,
|
| 153 |
+
) -> Tuple[str, str]:
|
| 154 |
+
"""Parse .openbb script.
|
| 155 |
+
|
| 156 |
+
Parameters
|
| 157 |
+
----------
|
| 158 |
+
raw_lines : List[str]
|
| 159 |
+
Lines from .openbb script
|
| 160 |
+
script_inputs: str, optional
|
| 161 |
+
Inputs to the script that come externally
|
| 162 |
+
|
| 163 |
+
Returns
|
| 164 |
+
-------
|
| 165 |
+
str
|
| 166 |
+
Error that occurred - if empty means no error
|
| 167 |
+
str
|
| 168 |
+
Processed string from .openbb script that can be run by the OpenBB Platform CLI
|
| 169 |
+
"""
|
| 170 |
+
ROUTINE_VARS: Dict[str, Union[str, List[str]]] = dict()
|
| 171 |
+
if script_inputs:
|
| 172 |
+
ROUTINE_VARS["$ARGV"] = script_inputs
|
| 173 |
+
|
| 174 |
+
## PRE PROCESSING
|
| 175 |
+
# Remove reset commands, comments, empty lines and trailing/leading whitespaces
|
| 176 |
+
raw_lines = [
|
| 177 |
+
x.strip()
|
| 178 |
+
for x in raw_lines
|
| 179 |
+
if (not is_reset(x)) and ("#" not in x) and x.strip()
|
| 180 |
+
]
|
| 181 |
+
|
| 182 |
+
## LOOK FOR NEW VARIABLES BEING DECLARED FROM USERS
|
| 183 |
+
lines_without_declarations = list()
|
| 184 |
+
for line in raw_lines:
|
| 185 |
+
# Check if this line has a variable attribution
|
| 186 |
+
# This currently allows user to override ARGV parameter
|
| 187 |
+
if "$" in line and "=" in line:
|
| 188 |
+
match = re.search(r"\$(\w+)\s*=\s*([\w\d,-.\s]+)", line)
|
| 189 |
+
if match:
|
| 190 |
+
VAR_NAME = match.group(1)
|
| 191 |
+
VAR_VALUES = match.group(2)
|
| 192 |
+
ROUTINE_VARS["$" + VAR_NAME] = (
|
| 193 |
+
VAR_VALUES if "," not in VAR_VALUES else VAR_VALUES.split(",")
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Just throw a warning when user uses wrong convention
|
| 197 |
+
numdollars = len(re.findall(r"\$", line))
|
| 198 |
+
if numdollars > 1:
|
| 199 |
+
session.console.print(
|
| 200 |
+
f"The variable {VAR_NAME} should not be declared as "
|
| 201 |
+
f"{'$' * numdollars}{VAR_NAME}. Instead it will be "
|
| 202 |
+
f"converted into ${VAR_NAME}."
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
else:
|
| 206 |
+
lines_without_declarations.append(line)
|
| 207 |
+
else:
|
| 208 |
+
lines_without_declarations.append(line)
|
| 209 |
+
|
| 210 |
+
# At this stage our ROUTINE_VARS should be completed coming from external AND from internal
|
| 211 |
+
# Now we want to replace the ROUTINE_VARS to where applicable throughout the .openbb script
|
| 212 |
+
# Due to this implementation, a variable declared at the end will still be effective
|
| 213 |
+
|
| 214 |
+
lines_with_vars_replaced = list()
|
| 215 |
+
foreach_loop_found = False
|
| 216 |
+
for line in lines_without_declarations:
|
| 217 |
+
# Save temporary line to ensure that all vars get replaced by correct vars
|
| 218 |
+
templine = line
|
| 219 |
+
|
| 220 |
+
# Found 'end' keyword which means that a loop has terminated
|
| 221 |
+
if re.match(r"^\s*end\s*$", line, re.IGNORECASE):
|
| 222 |
+
# Check whether the foreach loop has started or not
|
| 223 |
+
if not foreach_loop_found:
|
| 224 |
+
return (
|
| 225 |
+
"[red]The script has a foreach loop that terminates before it gets started. "
|
| 226 |
+
"Add the keyword 'foreach' to explicitly start loop[/red]",
|
| 227 |
+
"",
|
| 228 |
+
)
|
| 229 |
+
foreach_loop_found = False
|
| 230 |
+
|
| 231 |
+
else:
|
| 232 |
+
# Found 'foreach' keyword which means there needs to be a matching 'end'
|
| 233 |
+
if re.search(r"foreach", line, re.IGNORECASE):
|
| 234 |
+
foreach_loop_found = True
|
| 235 |
+
|
| 236 |
+
# Regular expression pattern to match variables starting with $
|
| 237 |
+
pattern = r"(?<!\$)(\$(\w+)(\[[^\]]*\])?)(?=(?:[^\]]*\]*))"
|
| 238 |
+
|
| 239 |
+
# Find all matches of the pattern in the line
|
| 240 |
+
matches: Optional[List[Match[str]]] = re.findall(pattern, line)
|
| 241 |
+
|
| 242 |
+
if matches:
|
| 243 |
+
for match in matches:
|
| 244 |
+
if match:
|
| 245 |
+
VAR_NAME = "$" + match[1]
|
| 246 |
+
VAR_SLICE = match[2][1:-1] if match[2] else ""
|
| 247 |
+
|
| 248 |
+
# Within a list refers to a single element
|
| 249 |
+
if VAR_SLICE.isdigit():
|
| 250 |
+
# This is an edge case for when the user has a variable such as $DATE = 2022-01-01
|
| 251 |
+
# We want the user to be able to access it with $DATE or $DATE[0] and the latest
|
| 252 |
+
# in python will only take the first '2'
|
| 253 |
+
if VAR_SLICE == "0":
|
| 254 |
+
if VAR_NAME in ROUTINE_VARS:
|
| 255 |
+
values = eval( # noqa: S307
|
| 256 |
+
f'ROUTINE_VARS["{VAR_NAME}"]'
|
| 257 |
+
)
|
| 258 |
+
if isinstance(values, list):
|
| 259 |
+
templine = templine.replace(
|
| 260 |
+
match[0],
|
| 261 |
+
eval(f"values[{VAR_SLICE}]"), # noqa: S307
|
| 262 |
+
)
|
| 263 |
+
else:
|
| 264 |
+
templine = templine.replace(match[0], values)
|
| 265 |
+
else:
|
| 266 |
+
return (
|
| 267 |
+
f"[red]Variable {VAR_NAME} not given "
|
| 268 |
+
"for current routine script.[/red]",
|
| 269 |
+
"",
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Only enters here when any other index from 0 is used
|
| 273 |
+
elif VAR_NAME in ROUTINE_VARS:
|
| 274 |
+
variable = eval( # noqa: S307
|
| 275 |
+
f'ROUTINE_VARS["{VAR_NAME}"]'
|
| 276 |
+
)
|
| 277 |
+
length_variable = (
|
| 278 |
+
len(variable) if isinstance(variable, list) else 1
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
# We use <= because we are using 0 index based lists
|
| 282 |
+
if length_variable <= int(VAR_SLICE):
|
| 283 |
+
return (
|
| 284 |
+
f"[red]Variable {VAR_NAME} only has "
|
| 285 |
+
f"{length_variable} elements and there "
|
| 286 |
+
f"was an attempt to access it with index {VAR_SLICE}.[/red]",
|
| 287 |
+
"",
|
| 288 |
+
)
|
| 289 |
+
templine = templine.replace(
|
| 290 |
+
match[0],
|
| 291 |
+
variable[int(VAR_SLICE)],
|
| 292 |
+
)
|
| 293 |
+
else:
|
| 294 |
+
return (
|
| 295 |
+
f"[red]Variable {VAR_NAME} not given for current routine script.[/red]",
|
| 296 |
+
"",
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
# Involves slicing which is a bit more tricky to use eval on
|
| 300 |
+
elif (
|
| 301 |
+
":" in VAR_SLICE
|
| 302 |
+
and len(VAR_SLICE.split(":")) == 2
|
| 303 |
+
and (
|
| 304 |
+
VAR_SLICE.split(":")[0].isdigit()
|
| 305 |
+
or VAR_SLICE.split(":")[1].isdigit()
|
| 306 |
+
)
|
| 307 |
+
):
|
| 308 |
+
slicing_tuple = "slice("
|
| 309 |
+
slicing_tuple += (
|
| 310 |
+
VAR_SLICE.split(":")[0]
|
| 311 |
+
if VAR_SLICE.split(":")[0].isdigit()
|
| 312 |
+
else "None"
|
| 313 |
+
)
|
| 314 |
+
slicing_tuple += ","
|
| 315 |
+
slicing_tuple += (
|
| 316 |
+
VAR_SLICE.split(":")[1]
|
| 317 |
+
if VAR_SLICE.split(":")[1].isdigit()
|
| 318 |
+
else "None"
|
| 319 |
+
)
|
| 320 |
+
slicing_tuple += ")"
|
| 321 |
+
|
| 322 |
+
vars_to_loop = eval( # noqa: S307
|
| 323 |
+
f'ROUTINE_VARS["{VAR_NAME}"][{slicing_tuple}]'
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Check whether the slicing was successful or not
|
| 327 |
+
if vars_to_loop:
|
| 328 |
+
templine = templine.replace(
|
| 329 |
+
match[0],
|
| 330 |
+
",".join(vars_to_loop),
|
| 331 |
+
)
|
| 332 |
+
else:
|
| 333 |
+
return (
|
| 334 |
+
f"[red]The foreach loop cannot run with input: {match[0]}.[/red]",
|
| 335 |
+
"",
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Just replace value without slicing or list
|
| 339 |
+
else:
|
| 340 |
+
if VAR_SLICE:
|
| 341 |
+
# Check if the string starts with a minus sign
|
| 342 |
+
if VAR_SLICE.startswith("-"):
|
| 343 |
+
if not VAR_SLICE[1:].isdigit():
|
| 344 |
+
return (
|
| 345 |
+
f"[red]Index '{VAR_SLICE}' is not a value[/red]",
|
| 346 |
+
"",
|
| 347 |
+
)
|
| 348 |
+
if int(VAR_SLICE) < 0:
|
| 349 |
+
return (
|
| 350 |
+
f"[red]Negative index on {VAR_NAME} is not allowed[/red]",
|
| 351 |
+
"",
|
| 352 |
+
)
|
| 353 |
+
if not VAR_SLICE.isdigit():
|
| 354 |
+
return (
|
| 355 |
+
f"[red]Index '{VAR_SLICE}' is not a value[/red]",
|
| 356 |
+
"",
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
if VAR_NAME in ROUTINE_VARS:
|
| 360 |
+
value = eval( # noqa: S307
|
| 361 |
+
f'ROUTINE_VARS["{VAR_NAME}"]'
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
# If the value is a list, we want to replace it with the whole list
|
| 365 |
+
if isinstance(value, list):
|
| 366 |
+
templine = templine.replace(
|
| 367 |
+
match[0],
|
| 368 |
+
",".join(value),
|
| 369 |
+
)
|
| 370 |
+
else:
|
| 371 |
+
templine = templine.replace(match[0], value)
|
| 372 |
+
|
| 373 |
+
else:
|
| 374 |
+
# Check if this is an OpenBB keyword variable like
|
| 375 |
+
# 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY
|
| 376 |
+
# and decode it into the right date if it exists
|
| 377 |
+
potential_date_match = (
|
| 378 |
+
match_and_return_openbb_keyword_date(VAR_NAME)
|
| 379 |
+
)
|
| 380 |
+
if potential_date_match:
|
| 381 |
+
templine = templine.replace(
|
| 382 |
+
match[0], potential_date_match
|
| 383 |
+
)
|
| 384 |
+
else:
|
| 385 |
+
return (
|
| 386 |
+
f"[red]Variable {VAR_NAME} not given for "
|
| 387 |
+
"current routine script.[/red]",
|
| 388 |
+
"",
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
lines_with_vars_replaced.append(templine)
|
| 392 |
+
|
| 393 |
+
# If this flags ends in True it means that the script routine has a foreach loop that never terminates
|
| 394 |
+
if foreach_loop_found:
|
| 395 |
+
return (
|
| 396 |
+
"[red]The script has a foreach loop that doesn't terminate. "
|
| 397 |
+
"Add the keyword 'end' to explicitly terminate loop[/red]",
|
| 398 |
+
"",
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
# Finally the only remaining thing to address are the foreach loops. For that we'll go through
|
| 402 |
+
# those lines and unroll the arguments that will be iterated by.
|
| 403 |
+
# Note that the fact that we checked before that the amount of foreach and end matches allow us
|
| 404 |
+
# to be confident that the script has no clear issues.
|
| 405 |
+
|
| 406 |
+
within_foreach = False
|
| 407 |
+
foreach_lines_loop: List[str] = list()
|
| 408 |
+
|
| 409 |
+
parsed_script = ""
|
| 410 |
+
final_lines = list()
|
| 411 |
+
varname = "VAR"
|
| 412 |
+
varused_inside = False
|
| 413 |
+
for line in lines_with_vars_replaced:
|
| 414 |
+
# Found 'foreach' header associated with loop
|
| 415 |
+
match = re.search(
|
| 416 |
+
r"foreach \$\$([A-Za-z\_]+) in ([A-Za-z0-9,-.]+)", line, re.IGNORECASE
|
| 417 |
+
)
|
| 418 |
+
if match:
|
| 419 |
+
varname = match.group(1)
|
| 420 |
+
foreach_loop = match.group(2).split(",")
|
| 421 |
+
within_foreach = True
|
| 422 |
+
|
| 423 |
+
# We are inside a loop and this is a line that we will want to replicate,
|
| 424 |
+
# so we need to temporarily store it until we reach the end
|
| 425 |
+
elif within_foreach:
|
| 426 |
+
# Found 'end' keyword which means that the foreach loop has reached the end
|
| 427 |
+
if re.match(r"^\s*end\s*$", line, re.IGNORECASE):
|
| 428 |
+
# Now we want to process what we were waiting for before
|
| 429 |
+
|
| 430 |
+
# Iterate through main foreach header
|
| 431 |
+
for var in foreach_loop:
|
| 432 |
+
# Iterate through all lines within foreach and end loop
|
| 433 |
+
for foreach_line_loop in foreach_lines_loop:
|
| 434 |
+
if f"$${varname}" in foreach_line_loop:
|
| 435 |
+
final_lines.append(
|
| 436 |
+
foreach_line_loop.replace(f"$${varname}", var).strip()
|
| 437 |
+
)
|
| 438 |
+
varused_inside = True
|
| 439 |
+
elif "$$" in foreach_line_loop:
|
| 440 |
+
return (
|
| 441 |
+
"[red]The script has a foreach loop that iterates through "
|
| 442 |
+
f"{','.join(foreach_loop)} with variable $${varname} "
|
| 443 |
+
"but another var name is being utilized instead[/red]",
|
| 444 |
+
"",
|
| 445 |
+
)
|
| 446 |
+
else:
|
| 447 |
+
final_lines.append(foreach_line_loop.strip())
|
| 448 |
+
|
| 449 |
+
if not varused_inside:
|
| 450 |
+
session.console.print(
|
| 451 |
+
f"The variable {varname} was used in foreach header "
|
| 452 |
+
"but it wasn't used inside the loop."
|
| 453 |
+
)
|
| 454 |
+
varused_inside = False
|
| 455 |
+
|
| 456 |
+
# Since this has been processed we reset the foreach loop lines
|
| 457 |
+
within_foreach = False
|
| 458 |
+
foreach_lines_loop = list()
|
| 459 |
+
|
| 460 |
+
else:
|
| 461 |
+
foreach_lines_loop.append(line)
|
| 462 |
+
|
| 463 |
+
else:
|
| 464 |
+
final_lines.append(line)
|
| 465 |
+
|
| 466 |
+
# If the list is non null, then we want to convert this into a parsed string that is
|
| 467 |
+
# recognized by the OpenBB Platform CLI
|
| 468 |
+
if final_lines:
|
| 469 |
+
parsed_script = f"{'/'.join([line.rstrip() for line in final_lines])}".replace(
|
| 470 |
+
"//", "/home/"
|
| 471 |
+
)
|
| 472 |
+
if parsed_script[0] == "/":
|
| 473 |
+
# If the user had added a / at the beginning, then it was converted to //home/
|
| 474 |
+
# and we need to remove it
|
| 475 |
+
if parsed_script.startswith("//home"):
|
| 476 |
+
parsed_script = parsed_script[6:]
|
| 477 |
+
else:
|
| 478 |
+
# We want the script to start from the home menu, hence we add it if the user
|
| 479 |
+
# didn't add it
|
| 480 |
+
parsed_script = "/" + parsed_script
|
| 481 |
+
|
| 482 |
+
# If the script finishes with // it means that we converted it to /home/
|
| 483 |
+
# This means that we are expecting a command to follow, but since this is
|
| 484 |
+
# the end of the script, we need to remove the trailing /
|
| 485 |
+
if parsed_script.endswith("/home/"):
|
| 486 |
+
parsed_script = parsed_script[:-1]
|
| 487 |
+
|
| 488 |
+
return "", parsed_script
|
cli/openbb_cli/controllers/settings_controller.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Settings Controller Module."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from functools import partial, update_wrapper
|
| 5 |
+
from types import MethodType
|
| 6 |
+
from typing import List, Literal, Optional, get_origin
|
| 7 |
+
|
| 8 |
+
from openbb_cli.config.menu_text import MenuText
|
| 9 |
+
from openbb_cli.controllers.base_controller import BaseController
|
| 10 |
+
from openbb_cli.models.settings import SettingGroups
|
| 11 |
+
from openbb_cli.session import Session
|
| 12 |
+
|
| 13 |
+
session = Session()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class SettingsController(BaseController):
|
| 17 |
+
"""Settings Controller class."""
|
| 18 |
+
|
| 19 |
+
_COMMANDS = {
|
| 20 |
+
v.json_schema_extra.get("command"): {
|
| 21 |
+
"command": (v.json_schema_extra or {}).get("command"),
|
| 22 |
+
"group": (v.json_schema_extra or {}).get("group"),
|
| 23 |
+
"description": v.description,
|
| 24 |
+
"annotation": v.annotation,
|
| 25 |
+
"field_name": k,
|
| 26 |
+
}
|
| 27 |
+
for k, v in sorted(
|
| 28 |
+
session.settings.model_fields.items(),
|
| 29 |
+
key=lambda item: (item[1].json_schema_extra or {}).get("command", ""),
|
| 30 |
+
)
|
| 31 |
+
if v.json_schema_extra
|
| 32 |
+
}
|
| 33 |
+
CHOICES_COMMANDS: List[str] = list(_COMMANDS.keys())
|
| 34 |
+
PATH = "/settings/"
|
| 35 |
+
CHOICES_GENERATION = True
|
| 36 |
+
|
| 37 |
+
def __init__(self, queue: Optional[List[str]] = None):
|
| 38 |
+
"""Initialize the Constructor."""
|
| 39 |
+
super().__init__(queue)
|
| 40 |
+
for cmd, field in self._COMMANDS.items():
|
| 41 |
+
group = field.get("group")
|
| 42 |
+
if group == SettingGroups.feature_flags:
|
| 43 |
+
self._generate_command(cmd, field, "toggle")
|
| 44 |
+
elif group == SettingGroups.preferences:
|
| 45 |
+
self._generate_command(cmd, field, "set")
|
| 46 |
+
self.update_completer(self.choices_default)
|
| 47 |
+
|
| 48 |
+
def print_help(self):
|
| 49 |
+
"""Print help."""
|
| 50 |
+
mt = MenuText("settings/")
|
| 51 |
+
mt.add_info("Feature Flags")
|
| 52 |
+
for k, f in self._COMMANDS.items():
|
| 53 |
+
if f.get("group") == SettingGroups.feature_flags:
|
| 54 |
+
mt.add_setting(
|
| 55 |
+
name=k,
|
| 56 |
+
status=getattr(session.settings, f["field_name"]),
|
| 57 |
+
description=f["description"],
|
| 58 |
+
)
|
| 59 |
+
mt.add_raw("\n")
|
| 60 |
+
mt.add_info("Preferences")
|
| 61 |
+
for k, f in self._COMMANDS.items():
|
| 62 |
+
if f.get("group") == SettingGroups.preferences:
|
| 63 |
+
mt.add_cmd(
|
| 64 |
+
name=k,
|
| 65 |
+
description=f["description"],
|
| 66 |
+
)
|
| 67 |
+
session.console.print(text=mt.menu_text, menu="Settings")
|
| 68 |
+
|
| 69 |
+
def _generate_command(
|
| 70 |
+
self, cmd_name: str, field: dict, action_type: Literal["toggle", "set"]
|
| 71 |
+
):
|
| 72 |
+
"""Generate command call."""
|
| 73 |
+
|
| 74 |
+
def _toggle(self, other_args: List[str], field=field) -> None:
|
| 75 |
+
"""Toggle setting value."""
|
| 76 |
+
field_name = field["field_name"]
|
| 77 |
+
parser = argparse.ArgumentParser(
|
| 78 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 79 |
+
prog=field["command"],
|
| 80 |
+
description=field["description"],
|
| 81 |
+
add_help=False,
|
| 82 |
+
)
|
| 83 |
+
ns_parser, _ = self.parse_simple_args(parser, other_args)
|
| 84 |
+
if ns_parser:
|
| 85 |
+
session.settings.set_item(
|
| 86 |
+
field_name, not getattr(session.settings, field_name)
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
def _set(self, other_args: List[str], field=field) -> None:
|
| 90 |
+
"""Set preference value."""
|
| 91 |
+
field_name = field["field_name"]
|
| 92 |
+
annotation = field["annotation"]
|
| 93 |
+
command = field["command"]
|
| 94 |
+
type_ = str if get_origin(annotation) is Literal else annotation
|
| 95 |
+
choices = None
|
| 96 |
+
if get_origin(annotation) is Literal:
|
| 97 |
+
choices = annotation.__args__
|
| 98 |
+
elif command == "console_style":
|
| 99 |
+
# To have updated choices for console style
|
| 100 |
+
choices = session.style.available_styles
|
| 101 |
+
parser = argparse.ArgumentParser(
|
| 102 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 103 |
+
prog=command,
|
| 104 |
+
description=field["description"],
|
| 105 |
+
add_help=False,
|
| 106 |
+
)
|
| 107 |
+
parser.add_argument(
|
| 108 |
+
"-v",
|
| 109 |
+
"--value",
|
| 110 |
+
dest="value",
|
| 111 |
+
action="store",
|
| 112 |
+
required=False,
|
| 113 |
+
type=type_, # type: ignore[arg-type]
|
| 114 |
+
choices=choices,
|
| 115 |
+
)
|
| 116 |
+
ns_parser, _ = self.parse_simple_args(parser, other_args)
|
| 117 |
+
if ns_parser:
|
| 118 |
+
if ns_parser.value:
|
| 119 |
+
# Console style is applied immediately
|
| 120 |
+
if command == "console_style":
|
| 121 |
+
session.style.apply(ns_parser.value)
|
| 122 |
+
session.settings.set_item(field_name, ns_parser.value)
|
| 123 |
+
session.console.print(
|
| 124 |
+
f"[info]Current value:[/info] {getattr(session.settings, field_name)}"
|
| 125 |
+
)
|
| 126 |
+
elif not other_args:
|
| 127 |
+
session.console.print(
|
| 128 |
+
f"[info]Current value:[/info] {getattr(session.settings, field_name)}"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
action = None
|
| 132 |
+
if action_type == "toggle":
|
| 133 |
+
action = _toggle
|
| 134 |
+
elif action_type == "set":
|
| 135 |
+
action = _set
|
| 136 |
+
else:
|
| 137 |
+
raise ValueError(f"Action type '{action_type}' not allowed.")
|
| 138 |
+
|
| 139 |
+
bound_method = update_wrapper(
|
| 140 |
+
wrapper=partial(MethodType(action, self), field=field), wrapped=action
|
| 141 |
+
)
|
| 142 |
+
setattr(self, f"call_{cmd_name}", bound_method)
|
cli/openbb_cli/controllers/utils.py
ADDED
|
@@ -0,0 +1,1032 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utils."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
import random
|
| 6 |
+
import re
|
| 7 |
+
import shutil
|
| 8 |
+
import sys
|
| 9 |
+
from contextlib import contextmanager
|
| 10 |
+
from datetime import (
|
| 11 |
+
datetime,
|
| 12 |
+
)
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import pandas as pd
|
| 18 |
+
import requests
|
| 19 |
+
from openbb_cli.config.constants import AVAILABLE_FLAIRS, ENV_FILE_SETTINGS
|
| 20 |
+
from openbb_cli.session import Session
|
| 21 |
+
from openbb_core.app.model.obbject import OBBject
|
| 22 |
+
from pytz import all_timezones, timezone
|
| 23 |
+
from rich.table import Table
|
| 24 |
+
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
from openbb_charting.core.openbb_figure import OpenBBFigure
|
| 27 |
+
|
| 28 |
+
# pylint: disable=R1702,R0912
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# pylint: disable=too-many-statements,no-member,too-many-branches,C0302
|
| 32 |
+
|
| 33 |
+
session = Session()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def remove_file(path: Path) -> bool:
|
| 37 |
+
"""Remove path.
|
| 38 |
+
|
| 39 |
+
Parameters
|
| 40 |
+
----------
|
| 41 |
+
path : Path
|
| 42 |
+
The file path.
|
| 43 |
+
|
| 44 |
+
Returns
|
| 45 |
+
-------
|
| 46 |
+
bool
|
| 47 |
+
The status of the removal.
|
| 48 |
+
"""
|
| 49 |
+
# TODO: Check why module level import leads to circular import.
|
| 50 |
+
try:
|
| 51 |
+
if os.path.isfile(path):
|
| 52 |
+
os.remove(path)
|
| 53 |
+
elif os.path.isdir(path):
|
| 54 |
+
shutil.rmtree(path)
|
| 55 |
+
return True
|
| 56 |
+
except Exception:
|
| 57 |
+
session.console.print(
|
| 58 |
+
f"\n[bold red]Failed to remove {path}"
|
| 59 |
+
"\nPlease delete this manually![/bold red]"
|
| 60 |
+
)
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def print_goodbye():
|
| 65 |
+
"""Print a goodbye message when quitting the terminal."""
|
| 66 |
+
# LEGACY GOODBYE MESSAGES - You'll live in our hearts forever.
|
| 67 |
+
# "An informed ape, is a strong ape."
|
| 68 |
+
# "Remember that stonks only go up."
|
| 69 |
+
# "Diamond hands."
|
| 70 |
+
# "Apes together strong."
|
| 71 |
+
# "This is our way."
|
| 72 |
+
# "Keep the spacesuit ape, we haven't reached the moon yet."
|
| 73 |
+
# "I am not a cat. I'm an ape."
|
| 74 |
+
# "We like the terminal."
|
| 75 |
+
# "...when offered a flight to the moon, nobody asks about what seat."
|
| 76 |
+
|
| 77 |
+
text = """
|
| 78 |
+
[param]Thank you for using the OpenBB Platform CLI and being part of this journey.[/param]
|
| 79 |
+
|
| 80 |
+
We hope you'll find the new OpenBB Platform CLI a valuable tool.
|
| 81 |
+
|
| 82 |
+
To stay tuned, sign up for our newsletter: [cmds]https://openbb.co/newsletter.[/]
|
| 83 |
+
|
| 84 |
+
Please feel free to check out our other products:
|
| 85 |
+
|
| 86 |
+
[bold]OpenBB Workspace[/]: [cmds]https://openbb.co[/cmds]
|
| 87 |
+
[bold]OpenBB Platform:[/] [cmds]https://docs.openbb.co/platform[/cmds]
|
| 88 |
+
[bold]OpenBB Bot[/]: [cmds]https://docs.openbb.co/bot[/cmds]
|
| 89 |
+
"""
|
| 90 |
+
session.console.print(text)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def print_guest_block_msg():
|
| 94 |
+
"""Block guest users from using the cli."""
|
| 95 |
+
if session.is_local():
|
| 96 |
+
session.console.print(
|
| 97 |
+
"[info]You are currently logged as a guest.[/info]\n"
|
| 98 |
+
"[info]Login to use this feature.[/info]\n\n"
|
| 99 |
+
"[info]If you don't have an account, you can create one here: [/info]"
|
| 100 |
+
f"[cmds]{session.settings.HUB_URL + '/register'}\n[/cmds]"
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def bootup():
|
| 105 |
+
"""Bootup the cli."""
|
| 106 |
+
if sys.platform == "win32":
|
| 107 |
+
# Enable VT100 Escape Sequence for WINDOWS 10 Ver. 1607
|
| 108 |
+
os.system("") # nosec # noqa: S605,S607
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
if os.name == "nt":
|
| 112 |
+
# pylint: disable=E1101
|
| 113 |
+
sys.stdin.reconfigure(encoding="utf-8") # type: ignore
|
| 114 |
+
# pylint: disable=E1101
|
| 115 |
+
sys.stdout.reconfigure(encoding="utf-8") # type: ignore
|
| 116 |
+
except Exception as e:
|
| 117 |
+
session.console.print(e, "\n")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def welcome_message():
|
| 121 |
+
"""Print the welcome message.
|
| 122 |
+
|
| 123 |
+
Prints first welcome message, help and a notification if updates are available.
|
| 124 |
+
"""
|
| 125 |
+
session.console.print(
|
| 126 |
+
f"\nWelcome to OpenBB Platform CLI v{session.settings.VERSION}"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def reset(queue: Optional[List[str]] = None):
|
| 131 |
+
"""Reset the CLI.
|
| 132 |
+
|
| 133 |
+
Allows for checking code without quitting.
|
| 134 |
+
"""
|
| 135 |
+
session.console.print("resetting...")
|
| 136 |
+
debug = session.settings.DEBUG_MODE
|
| 137 |
+
dev = session.settings.DEV_BACKEND
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
# remove the hub routines
|
| 141 |
+
if not session.is_local():
|
| 142 |
+
remove_file(
|
| 143 |
+
Path(session.user.preferences.export_directory, "routines", "hub")
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# if not get_current_user().profile.remember:
|
| 147 |
+
# Local.remove(HIST_FILE_PROMPT)
|
| 148 |
+
|
| 149 |
+
# we clear all openbb_cli modules from sys.modules
|
| 150 |
+
for module in list(sys.modules.keys()):
|
| 151 |
+
parts = module.split(".")
|
| 152 |
+
if parts[0] == "openbb_cli":
|
| 153 |
+
del sys.modules[module]
|
| 154 |
+
|
| 155 |
+
queue_list = ["/".join(queue) if len(queue) > 0 else ""] # type: ignore
|
| 156 |
+
# pylint: disable=import-outside-toplevel
|
| 157 |
+
# we run the cli again
|
| 158 |
+
if session.is_local():
|
| 159 |
+
from openbb_cli.controllers.cli_controller import main
|
| 160 |
+
|
| 161 |
+
main(debug, dev, queue_list, module="") # type: ignore
|
| 162 |
+
else:
|
| 163 |
+
from openbb_cli.controllers.cli_controller import launch
|
| 164 |
+
|
| 165 |
+
launch(queue=queue_list)
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
session.console.print(f"Unfortunately, resetting wasn't possible: {e}\n")
|
| 169 |
+
print_goodbye()
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@contextmanager
|
| 173 |
+
def suppress_stdout():
|
| 174 |
+
"""Suppress the stdout."""
|
| 175 |
+
with open(os.devnull, "w") as devnull:
|
| 176 |
+
old_stdout = sys.stdout
|
| 177 |
+
old_stderr = sys.stderr
|
| 178 |
+
sys.stdout = devnull
|
| 179 |
+
sys.stderr = devnull
|
| 180 |
+
try:
|
| 181 |
+
yield
|
| 182 |
+
finally:
|
| 183 |
+
sys.stdout = old_stdout
|
| 184 |
+
sys.stderr = old_stderr
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def first_time_user() -> bool:
|
| 188 |
+
"""Check whether a user is a first time user.
|
| 189 |
+
|
| 190 |
+
A first time user is someone with an empty .env file.
|
| 191 |
+
If this is true, it also adds an env variable to make sure this does not run again.
|
| 192 |
+
|
| 193 |
+
Returns
|
| 194 |
+
-------
|
| 195 |
+
bool
|
| 196 |
+
Whether or not the user is a first time user
|
| 197 |
+
"""
|
| 198 |
+
if ENV_FILE_SETTINGS.stat().st_size == 0:
|
| 199 |
+
session.settings.set_item("PREVIOUS_USE", True)
|
| 200 |
+
return True
|
| 201 |
+
return False
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def parse_and_split_input(an_input: str, custom_filters: List) -> List[str]:
|
| 205 |
+
"""Filter and split the input queue.
|
| 206 |
+
|
| 207 |
+
Uses regex to filters command arguments that have forward slashes so that it doesn't
|
| 208 |
+
break the execution of the command queue.
|
| 209 |
+
Currently handles unix paths and sorting settings for screener menus.
|
| 210 |
+
|
| 211 |
+
Parameters
|
| 212 |
+
----------
|
| 213 |
+
an_input : str
|
| 214 |
+
User input as string
|
| 215 |
+
custom_filters : List
|
| 216 |
+
Additional regular expressions to match
|
| 217 |
+
|
| 218 |
+
Returns
|
| 219 |
+
-------
|
| 220 |
+
List[str]
|
| 221 |
+
Command queue as list
|
| 222 |
+
"""
|
| 223 |
+
# Make sure that the user can go back to the root when doing "/"
|
| 224 |
+
if an_input and an_input == "/":
|
| 225 |
+
an_input = "home"
|
| 226 |
+
|
| 227 |
+
# everything from ` -f ` to the next known extension
|
| 228 |
+
file_flag = r"(\ -f |\ --file )"
|
| 229 |
+
up_to = r".*?"
|
| 230 |
+
known_extensions = r"(\.(xlsx|csv|xls|tsv|json|yaml|ini|openbb|ipynb))"
|
| 231 |
+
unix_path_arg_exp = f"({file_flag}{up_to}{known_extensions})"
|
| 232 |
+
|
| 233 |
+
# Add custom expressions to handle edge cases of individual controllers
|
| 234 |
+
custom_filter = ""
|
| 235 |
+
for exp in custom_filters:
|
| 236 |
+
if exp is not None:
|
| 237 |
+
custom_filter += f"|{exp}"
|
| 238 |
+
del exp
|
| 239 |
+
|
| 240 |
+
slash_filter_exp = f"({unix_path_arg_exp}){custom_filter}"
|
| 241 |
+
|
| 242 |
+
filter_input = True
|
| 243 |
+
placeholders: Dict[str, str] = {}
|
| 244 |
+
while filter_input:
|
| 245 |
+
match = re.search(pattern=slash_filter_exp, string=an_input)
|
| 246 |
+
if match is not None:
|
| 247 |
+
placeholder = f"{{placeholder{len(placeholders)+1}}}"
|
| 248 |
+
placeholders[placeholder] = an_input[
|
| 249 |
+
match.span()[0] : match.span()[1] # noqa:E203
|
| 250 |
+
]
|
| 251 |
+
an_input = (
|
| 252 |
+
an_input[: match.span()[0]]
|
| 253 |
+
+ placeholder
|
| 254 |
+
+ an_input[match.span()[1] :] # noqa:E203
|
| 255 |
+
)
|
| 256 |
+
else:
|
| 257 |
+
filter_input = False
|
| 258 |
+
|
| 259 |
+
commands = an_input.split("/") if "timezone" not in an_input else [an_input]
|
| 260 |
+
|
| 261 |
+
for command_num, command in enumerate(commands):
|
| 262 |
+
if command == commands[-1] == "":
|
| 263 |
+
return list(filter(None, commands))
|
| 264 |
+
matching_placeholders = [tag for tag in placeholders if tag in command]
|
| 265 |
+
if len(matching_placeholders) > 0:
|
| 266 |
+
for tag in matching_placeholders:
|
| 267 |
+
commands[command_num] = command.replace(tag, placeholders[tag])
|
| 268 |
+
return commands
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def return_colored_value(value: str):
|
| 272 |
+
"""Return the string value based on condition.
|
| 273 |
+
|
| 274 |
+
Return it with green, yellow, red or white color based on
|
| 275 |
+
whether the number is positive, negative, zero or other, respectively.
|
| 276 |
+
|
| 277 |
+
Parameters
|
| 278 |
+
----------
|
| 279 |
+
value: str
|
| 280 |
+
string to be checked
|
| 281 |
+
|
| 282 |
+
Returns
|
| 283 |
+
-------
|
| 284 |
+
value: str
|
| 285 |
+
string with color based on value of number if it exists
|
| 286 |
+
"""
|
| 287 |
+
values = re.findall(r"[-+]?(?:\d*\.\d+|\d+)", value)
|
| 288 |
+
|
| 289 |
+
# Finds exactly 1 number in the string
|
| 290 |
+
if len(values) == 1:
|
| 291 |
+
if float(values[0]) > 0:
|
| 292 |
+
return f"[green]{value}[/green]"
|
| 293 |
+
|
| 294 |
+
if float(values[0]) < 0:
|
| 295 |
+
return f"[red]{value}[/red]"
|
| 296 |
+
|
| 297 |
+
if float(values[0]) == 0:
|
| 298 |
+
return f"[yellow]{value}[/yellow]"
|
| 299 |
+
|
| 300 |
+
return f"{value}"
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
| 304 |
+
def print_rich_table( # noqa: PLR0912
|
| 305 |
+
df: pd.DataFrame,
|
| 306 |
+
show_index: bool = False,
|
| 307 |
+
title: str = "",
|
| 308 |
+
index_name: str = "",
|
| 309 |
+
headers: Optional[Union[List[str], pd.Index]] = None,
|
| 310 |
+
floatfmt: Union[str, List[str]] = ".2f",
|
| 311 |
+
show_header: bool = True,
|
| 312 |
+
automatic_coloring: bool = False,
|
| 313 |
+
columns_to_auto_color: Optional[List[str]] = None,
|
| 314 |
+
rows_to_auto_color: Optional[List[str]] = None,
|
| 315 |
+
export: bool = False,
|
| 316 |
+
limit: Optional[int] = 1000,
|
| 317 |
+
columns_keep_types: Optional[List[str]] = None,
|
| 318 |
+
use_tabulate_df: bool = True,
|
| 319 |
+
):
|
| 320 |
+
"""Prepare a table from df in rich.
|
| 321 |
+
|
| 322 |
+
Parameters
|
| 323 |
+
----------
|
| 324 |
+
df: pd.DataFrame
|
| 325 |
+
Dataframe to turn into table
|
| 326 |
+
show_index: bool
|
| 327 |
+
Whether to include index
|
| 328 |
+
title: str
|
| 329 |
+
Title for table
|
| 330 |
+
index_name : str
|
| 331 |
+
Title for index column
|
| 332 |
+
headers: List[str]
|
| 333 |
+
Titles for columns
|
| 334 |
+
floatfmt: Union[str, List[str]]
|
| 335 |
+
Float number formatting specs as string or list of strings. Defaults to ".2f"
|
| 336 |
+
show_header: bool
|
| 337 |
+
Whether to show the header row.
|
| 338 |
+
automatic_coloring: bool
|
| 339 |
+
Automatically color a table based on positive and negative values
|
| 340 |
+
columns_to_auto_color: List[str]
|
| 341 |
+
Columns to automatically color
|
| 342 |
+
rows_to_auto_color: List[str]
|
| 343 |
+
Rows to automatically color
|
| 344 |
+
export: bool
|
| 345 |
+
Whether we are exporting the table to a file. If so, we don't want to print it.
|
| 346 |
+
limit: Optional[int]
|
| 347 |
+
Limit the number of rows to show.
|
| 348 |
+
columns_keep_types: Optional[List[str]]
|
| 349 |
+
Columns to keep their types, i.e. not convert to numeric
|
| 350 |
+
"""
|
| 351 |
+
if export:
|
| 352 |
+
return
|
| 353 |
+
|
| 354 |
+
MAX_COLS = session.settings.ALLOWED_NUMBER_OF_COLUMNS
|
| 355 |
+
MAX_ROWS = session.settings.ALLOWED_NUMBER_OF_ROWS
|
| 356 |
+
|
| 357 |
+
# Make a copy of the dataframe to avoid SettingWithCopyWarning
|
| 358 |
+
df = df.copy()
|
| 359 |
+
|
| 360 |
+
show_index = not isinstance(df.index, pd.RangeIndex) and show_index
|
| 361 |
+
# convert non-str that are not timestamp or int into str
|
| 362 |
+
# eg) praw.models.reddit.subreddit.Subreddit
|
| 363 |
+
for col in df.columns:
|
| 364 |
+
if columns_keep_types is not None and col in columns_keep_types:
|
| 365 |
+
continue
|
| 366 |
+
try:
|
| 367 |
+
if not any(
|
| 368 |
+
isinstance(df[col].iloc[x], pd.Timestamp)
|
| 369 |
+
for x in range(min(10, len(df)))
|
| 370 |
+
):
|
| 371 |
+
df[col] = df[col].apply(pd.to_numeric)
|
| 372 |
+
except (ValueError, TypeError):
|
| 373 |
+
df[col] = df[col].astype(str)
|
| 374 |
+
|
| 375 |
+
def _get_headers(_headers: Union[List[str], pd.Index]) -> List[str]:
|
| 376 |
+
"""Check if headers are valid and return them."""
|
| 377 |
+
output = _headers
|
| 378 |
+
if isinstance(_headers, pd.Index):
|
| 379 |
+
output = list(_headers)
|
| 380 |
+
if len(output) != len(df.columns):
|
| 381 |
+
raise ValueError("Length of headers does not match length of DataFrame.")
|
| 382 |
+
return output # type: ignore
|
| 383 |
+
|
| 384 |
+
if session.settings.USE_INTERACTIVE_DF:
|
| 385 |
+
df_outgoing = df.copy()
|
| 386 |
+
# If headers are provided, use them
|
| 387 |
+
if headers is not None:
|
| 388 |
+
# We check if headers are valid
|
| 389 |
+
df_outgoing.columns = _get_headers(headers)
|
| 390 |
+
|
| 391 |
+
if show_index and index_name not in df_outgoing.columns:
|
| 392 |
+
# If index name is provided, we use it
|
| 393 |
+
df_outgoing.index.name = index_name or "Index"
|
| 394 |
+
df_outgoing = df_outgoing.reset_index()
|
| 395 |
+
|
| 396 |
+
for col in df_outgoing.columns:
|
| 397 |
+
if col == "":
|
| 398 |
+
df_outgoing = df_outgoing.rename(columns={col: " "})
|
| 399 |
+
|
| 400 |
+
session._backend.send_table( # type: ignore # pylint: disable=protected-access
|
| 401 |
+
df_table=df_outgoing,
|
| 402 |
+
title=title,
|
| 403 |
+
theme=session.user.preferences.table_style,
|
| 404 |
+
)
|
| 405 |
+
return
|
| 406 |
+
|
| 407 |
+
df = df.copy() if not limit else df.copy().iloc[:limit]
|
| 408 |
+
if automatic_coloring:
|
| 409 |
+
if columns_to_auto_color:
|
| 410 |
+
for col in columns_to_auto_color:
|
| 411 |
+
# checks whether column exists
|
| 412 |
+
if col in df.columns:
|
| 413 |
+
df[col] = df[col].apply(lambda x: return_colored_value(str(x)))
|
| 414 |
+
if rows_to_auto_color:
|
| 415 |
+
for row in rows_to_auto_color:
|
| 416 |
+
# checks whether row exists
|
| 417 |
+
if row in df.index:
|
| 418 |
+
df.loc[row] = df.loc[row].apply(
|
| 419 |
+
lambda x: return_colored_value(str(x))
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
if columns_to_auto_color is None and rows_to_auto_color is None:
|
| 423 |
+
df = df.applymap(lambda x: return_colored_value(str(x)))
|
| 424 |
+
|
| 425 |
+
exceeds_allowed_columns = len(df.columns) > MAX_COLS
|
| 426 |
+
exceeds_allowed_rows = len(df) > MAX_ROWS
|
| 427 |
+
|
| 428 |
+
if exceeds_allowed_columns:
|
| 429 |
+
original_columns = df.columns.tolist()
|
| 430 |
+
trimmed_columns = df.columns.tolist()[:MAX_COLS]
|
| 431 |
+
df = df[trimmed_columns]
|
| 432 |
+
trimmed_columns = [
|
| 433 |
+
col for col in original_columns if col not in trimmed_columns
|
| 434 |
+
]
|
| 435 |
+
|
| 436 |
+
if exceeds_allowed_rows:
|
| 437 |
+
n_rows = len(df.index)
|
| 438 |
+
max_rows = MAX_ROWS
|
| 439 |
+
df = df[:max_rows]
|
| 440 |
+
trimmed_rows_count = n_rows - max_rows
|
| 441 |
+
|
| 442 |
+
if use_tabulate_df:
|
| 443 |
+
table = Table(title=title, show_lines=True, show_header=show_header)
|
| 444 |
+
|
| 445 |
+
if show_index:
|
| 446 |
+
table.add_column(index_name)
|
| 447 |
+
|
| 448 |
+
if headers is not None:
|
| 449 |
+
headers = _get_headers(headers)
|
| 450 |
+
for header in headers:
|
| 451 |
+
table.add_column(str(header))
|
| 452 |
+
else:
|
| 453 |
+
for column in df.columns:
|
| 454 |
+
table.add_column(str(column))
|
| 455 |
+
|
| 456 |
+
if isinstance(floatfmt, list) and len(floatfmt) != len(df.columns):
|
| 457 |
+
raise (
|
| 458 |
+
ValueError(
|
| 459 |
+
"Length of floatfmt list does not match length of DataFrame columns."
|
| 460 |
+
)
|
| 461 |
+
)
|
| 462 |
+
if isinstance(floatfmt, str):
|
| 463 |
+
floatfmt = [floatfmt for _ in range(len(df.columns))]
|
| 464 |
+
|
| 465 |
+
for idx, values in zip(df.index.tolist(), df.values.tolist()):
|
| 466 |
+
# remove hour/min/sec from timestamp index - Format: YYYY-MM-DD # make better
|
| 467 |
+
row_idx = [str(idx)] if show_index else []
|
| 468 |
+
row_idx += [
|
| 469 |
+
(
|
| 470 |
+
str(x)
|
| 471 |
+
if not isinstance(x, float) and not isinstance(x, np.float64)
|
| 472 |
+
else (
|
| 473 |
+
f"{x:{floatfmt[idx]}}"
|
| 474 |
+
if isinstance(floatfmt, list)
|
| 475 |
+
else (
|
| 476 |
+
f"{x:.2e}"
|
| 477 |
+
if 0 < abs(float(x)) <= 0.0001
|
| 478 |
+
else f"{x:floatfmt}"
|
| 479 |
+
)
|
| 480 |
+
)
|
| 481 |
+
)
|
| 482 |
+
for idx, x in enumerate(values)
|
| 483 |
+
]
|
| 484 |
+
table.add_row(*row_idx)
|
| 485 |
+
session.console.print(table)
|
| 486 |
+
else:
|
| 487 |
+
session.console.print(df.to_string(col_space=0))
|
| 488 |
+
|
| 489 |
+
if exceeds_allowed_columns:
|
| 490 |
+
session.console.print(
|
| 491 |
+
f"[yellow]\nAllowed number of columns exceeded ({session.settings.ALLOWED_NUMBER_OF_COLUMNS}).\n"
|
| 492 |
+
f"The following columns were removed from the output: {', '.join(trimmed_columns)}.\n[/yellow]"
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
if exceeds_allowed_rows:
|
| 496 |
+
session.console.print(
|
| 497 |
+
f"[yellow]\nAllowed number of rows exceeded ({session.settings.ALLOWED_NUMBER_OF_ROWS}).\n"
|
| 498 |
+
f"{trimmed_rows_count} rows were removed from the output.\n[/yellow]"
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
if exceeds_allowed_columns or exceeds_allowed_rows:
|
| 502 |
+
session.console.print(
|
| 503 |
+
"Use the `--export` flag to analyse the full output on a file."
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def check_non_negative(value) -> int:
|
| 508 |
+
"""Argparse type to check non negative int."""
|
| 509 |
+
new_value = int(value)
|
| 510 |
+
if new_value < 0:
|
| 511 |
+
raise argparse.ArgumentTypeError(f"{value} is negative")
|
| 512 |
+
return new_value
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def check_positive(value) -> int:
|
| 516 |
+
"""Argparse type to check positive int."""
|
| 517 |
+
new_value = int(value)
|
| 518 |
+
if new_value <= 0:
|
| 519 |
+
raise argparse.ArgumentTypeError(f"{value} is an invalid positive int value")
|
| 520 |
+
return new_value
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def validate_register_key(value: str) -> str:
|
| 524 |
+
"""Validate the register key to ensure it does not contain the reserved word 'OBB'."""
|
| 525 |
+
if "OBB" in value:
|
| 526 |
+
raise argparse.ArgumentTypeError(
|
| 527 |
+
"The register key cannot contain the reserved word 'OBB'."
|
| 528 |
+
)
|
| 529 |
+
return str(value)
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def get_user_agent() -> str:
|
| 533 |
+
"""Get a not very random user agent."""
|
| 534 |
+
user_agent_strings = [
|
| 535 |
+
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.10; rv:86.1) Gecko/20100101 Firefox/86.1",
|
| 536 |
+
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:86.1) Gecko/20100101 Firefox/86.1",
|
| 537 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:82.1) Gecko/20100101 Firefox/82.1",
|
| 538 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:86.0) Gecko/20100101 Firefox/86.0",
|
| 539 |
+
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:86.0) Gecko/20100101 Firefox/86.0",
|
| 540 |
+
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.10; rv:83.0) Gecko/20100101 Firefox/83.0",
|
| 541 |
+
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:84.0) Gecko/20100101 Firefox/84.0",
|
| 542 |
+
]
|
| 543 |
+
|
| 544 |
+
return random.choice(user_agent_strings) # nosec # noqa: S311
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def get_flair() -> str:
|
| 548 |
+
"""Get a flair icon."""
|
| 549 |
+
current_flair = str(session.settings.FLAIR)
|
| 550 |
+
flair = AVAILABLE_FLAIRS.get(current_flair, current_flair)
|
| 551 |
+
return flair
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
def get_dtime() -> str:
|
| 555 |
+
"""Get a datetime string."""
|
| 556 |
+
dtime = ""
|
| 557 |
+
if session.settings.USE_DATETIME and get_user_timezone_or_invalid() != "INVALID":
|
| 558 |
+
dtime = datetime.now(timezone(get_user_timezone())).strftime("%Y %b %d, %H:%M")
|
| 559 |
+
return dtime
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
def get_flair_and_username() -> str:
|
| 563 |
+
"""Get a flair icon and username."""
|
| 564 |
+
flair = get_flair()
|
| 565 |
+
if dtime := get_dtime():
|
| 566 |
+
dtime = f"{dtime} "
|
| 567 |
+
|
| 568 |
+
username = getattr(session.user.profile.hub_session, "username", "")
|
| 569 |
+
if username:
|
| 570 |
+
username = f"[{username}] "
|
| 571 |
+
|
| 572 |
+
return f"{dtime}{username}{flair}"
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
def is_timezone_valid(user_tz: str) -> bool:
|
| 576 |
+
"""Check whether user timezone is valid.
|
| 577 |
+
|
| 578 |
+
Parameters
|
| 579 |
+
----------
|
| 580 |
+
user_tz: str
|
| 581 |
+
Timezone to check for validity
|
| 582 |
+
|
| 583 |
+
Returns
|
| 584 |
+
-------
|
| 585 |
+
bool
|
| 586 |
+
True if timezone provided is valid
|
| 587 |
+
"""
|
| 588 |
+
return user_tz in all_timezones
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
def get_user_timezone() -> str:
|
| 592 |
+
"""Get user timezone if it is a valid one.
|
| 593 |
+
|
| 594 |
+
Returns
|
| 595 |
+
-------
|
| 596 |
+
str
|
| 597 |
+
user timezone based on .env file
|
| 598 |
+
"""
|
| 599 |
+
return session.settings.TIMEZONE
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def get_user_timezone_or_invalid() -> str:
|
| 603 |
+
"""Get user timezone if it is a valid one.
|
| 604 |
+
|
| 605 |
+
Returns
|
| 606 |
+
-------
|
| 607 |
+
str
|
| 608 |
+
user timezone based on timezone.openbb file or INVALID
|
| 609 |
+
"""
|
| 610 |
+
user_tz = get_user_timezone()
|
| 611 |
+
if is_timezone_valid(user_tz):
|
| 612 |
+
return f"{user_tz}"
|
| 613 |
+
return "INVALID"
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
def check_file_type_saved(valid_types: Optional[List[str]] = None):
|
| 617 |
+
"""Provide valid types for the user to be able to select.
|
| 618 |
+
|
| 619 |
+
Parameters
|
| 620 |
+
----------
|
| 621 |
+
valid_types: List[str]
|
| 622 |
+
List of valid types to export data
|
| 623 |
+
|
| 624 |
+
Returns
|
| 625 |
+
-------
|
| 626 |
+
check_filenames: Optional[List[str]]
|
| 627 |
+
Function that returns list of filenames to export data
|
| 628 |
+
"""
|
| 629 |
+
|
| 630 |
+
def check_filenames(filenames: str = "") -> str:
|
| 631 |
+
"""Check if filenames are valid.
|
| 632 |
+
|
| 633 |
+
Parameters
|
| 634 |
+
----------
|
| 635 |
+
filenames: str
|
| 636 |
+
filenames to be saved separated with comma
|
| 637 |
+
|
| 638 |
+
Returns
|
| 639 |
+
----------
|
| 640 |
+
str
|
| 641 |
+
valid filenames separated with comma
|
| 642 |
+
"""
|
| 643 |
+
if not filenames or not valid_types:
|
| 644 |
+
return ""
|
| 645 |
+
valid_filenames = list()
|
| 646 |
+
for filename in filenames.split(","):
|
| 647 |
+
if filename.endswith(tuple(valid_types)):
|
| 648 |
+
valid_filenames.append(filename)
|
| 649 |
+
else:
|
| 650 |
+
session.console.print(
|
| 651 |
+
f"[red]Filename '{filename}' provided is not valid!\nPlease use one of the following file types:"
|
| 652 |
+
f"{','.join(valid_types)}[/red]\n"
|
| 653 |
+
)
|
| 654 |
+
return ",".join(valid_filenames)
|
| 655 |
+
|
| 656 |
+
return check_filenames
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def remove_timezone_from_dataframe(df: pd.DataFrame) -> pd.DataFrame:
|
| 660 |
+
"""Remove timezone information from a dataframe.
|
| 661 |
+
|
| 662 |
+
Parameters
|
| 663 |
+
----------
|
| 664 |
+
df : pd.DataFrame
|
| 665 |
+
The dataframe to remove timezone information from
|
| 666 |
+
|
| 667 |
+
Returns
|
| 668 |
+
-------
|
| 669 |
+
pd.DataFrame
|
| 670 |
+
The dataframe with timezone information removed
|
| 671 |
+
"""
|
| 672 |
+
date_cols = []
|
| 673 |
+
index_is_date = False
|
| 674 |
+
|
| 675 |
+
# Find columns and index containing date data
|
| 676 |
+
if (
|
| 677 |
+
df.index.dtype.kind == "M"
|
| 678 |
+
and hasattr(df.index.dtype, "tz")
|
| 679 |
+
and df.index.dtype.tz is not None
|
| 680 |
+
):
|
| 681 |
+
index_is_date = True
|
| 682 |
+
|
| 683 |
+
for col, dtype in df.dtypes.items():
|
| 684 |
+
if dtype.kind == "M" and hasattr(df.index.dtype, "tz") and dtype.tz is not None:
|
| 685 |
+
date_cols.append(col)
|
| 686 |
+
|
| 687 |
+
# Remove the timezone information
|
| 688 |
+
for col in date_cols:
|
| 689 |
+
df[col] = df[col].dt.date
|
| 690 |
+
|
| 691 |
+
if index_is_date:
|
| 692 |
+
index_name = df.index.name
|
| 693 |
+
df.index = df.index.date
|
| 694 |
+
df.index.name = index_name
|
| 695 |
+
|
| 696 |
+
return df
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
def compose_export_path(func_name: str, dir_path: str) -> Path:
|
| 700 |
+
"""Compose export path for data from the terminal.
|
| 701 |
+
|
| 702 |
+
Creates a path to a folder and a filename based on conditions.
|
| 703 |
+
|
| 704 |
+
Parameters
|
| 705 |
+
----------
|
| 706 |
+
func_name : str
|
| 707 |
+
Name of the command that invokes this function
|
| 708 |
+
dir_path : str
|
| 709 |
+
Path of directory from where this function is called
|
| 710 |
+
|
| 711 |
+
Returns
|
| 712 |
+
-------
|
| 713 |
+
Path
|
| 714 |
+
Path variable containing the path of the exported file
|
| 715 |
+
"""
|
| 716 |
+
now = datetime.now()
|
| 717 |
+
# Resolving all symlinks and also normalizing path.
|
| 718 |
+
resolve_path = Path(dir_path).resolve()
|
| 719 |
+
# Getting the directory names from the path. Instead of using split/replace (Windows doesn't like that)
|
| 720 |
+
# check if this is done in a main context to avoid saving with openbb_cli
|
| 721 |
+
if resolve_path.parts[-2] == "openbb_cli":
|
| 722 |
+
path_cmd = f"{resolve_path.parts[-1]}"
|
| 723 |
+
else:
|
| 724 |
+
path_cmd = f"{resolve_path.parts[-2]}_{resolve_path.parts[-1]}"
|
| 725 |
+
|
| 726 |
+
default_filename = f"{now.strftime('%Y%m%d_%H%M%S')}_{path_cmd}_{func_name}"
|
| 727 |
+
|
| 728 |
+
full_path = Path(session.user.preferences.export_directory) / default_filename
|
| 729 |
+
|
| 730 |
+
return full_path
|
| 731 |
+
|
| 732 |
+
|
| 733 |
+
def ask_file_overwrite(file_path: Path) -> Tuple[bool, bool]:
|
| 734 |
+
"""Provide a prompt for overwriting existing files.
|
| 735 |
+
|
| 736 |
+
Returns two values, the first is a boolean indicating if the file exists and the
|
| 737 |
+
second is a boolean indicating if the user wants to overwrite the file.
|
| 738 |
+
"""
|
| 739 |
+
if session.settings.FILE_OVERWRITE:
|
| 740 |
+
return False, True
|
| 741 |
+
if session.settings.TEST_MODE:
|
| 742 |
+
return False, True
|
| 743 |
+
if file_path.exists():
|
| 744 |
+
overwrite = input("\nFile already exists. Overwrite? [y/n]: ").lower()
|
| 745 |
+
if overwrite == "y":
|
| 746 |
+
file_path.unlink(missing_ok=True)
|
| 747 |
+
# File exists and user wants to overwrite
|
| 748 |
+
return True, True
|
| 749 |
+
# File exists and user does not want to overwrite
|
| 750 |
+
return True, False
|
| 751 |
+
# File does not exist
|
| 752 |
+
return False, True
|
| 753 |
+
|
| 754 |
+
|
| 755 |
+
# This is a false positive on pylint and being tracked in pylint #3060
|
| 756 |
+
# pylint: disable=abstract-class-instantiated,too-many-positional-arguments
|
| 757 |
+
def save_to_excel(df, saved_path, sheet_name, start_row=0, index=True, header=True):
|
| 758 |
+
"""Save a Pandas DataFrame to an Excel file.
|
| 759 |
+
|
| 760 |
+
Args:
|
| 761 |
+
df: A Pandas DataFrame.
|
| 762 |
+
saved_path: The path to the Excel file to save to.
|
| 763 |
+
sheet_name: The name of the sheet to save the DataFrame to.
|
| 764 |
+
start_row: The row number to start writing the DataFrame at.
|
| 765 |
+
index: Whether to write the DataFrame index to the Excel file.
|
| 766 |
+
header: Whether to write the DataFrame header to the Excel file.
|
| 767 |
+
"""
|
| 768 |
+
overwrite_options = {
|
| 769 |
+
"o": "replace",
|
| 770 |
+
"a": "overlay",
|
| 771 |
+
"n": "new",
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
if not saved_path.exists():
|
| 775 |
+
with pd.ExcelWriter(saved_path, engine="openpyxl") as writer:
|
| 776 |
+
df.to_excel(writer, sheet_name=sheet_name, index=index, header=header)
|
| 777 |
+
|
| 778 |
+
else:
|
| 779 |
+
with pd.ExcelFile(saved_path) as reader:
|
| 780 |
+
overwrite_option = "n"
|
| 781 |
+
if sheet_name in reader.sheet_names:
|
| 782 |
+
overwrite_option = input(
|
| 783 |
+
"\nSheet already exists. Overwrite/Append/New? [o/a/n]: "
|
| 784 |
+
).lower()
|
| 785 |
+
start_row = 0
|
| 786 |
+
if overwrite_option == "a":
|
| 787 |
+
existing_df = pd.read_excel(saved_path, sheet_name=sheet_name)
|
| 788 |
+
start_row = existing_df.shape[0] + 1
|
| 789 |
+
|
| 790 |
+
with pd.ExcelWriter(
|
| 791 |
+
saved_path,
|
| 792 |
+
mode="a",
|
| 793 |
+
if_sheet_exists=overwrite_options[overwrite_option],
|
| 794 |
+
engine="openpyxl",
|
| 795 |
+
) as writer:
|
| 796 |
+
df.to_excel(
|
| 797 |
+
writer,
|
| 798 |
+
sheet_name=sheet_name,
|
| 799 |
+
startrow=start_row,
|
| 800 |
+
index=index,
|
| 801 |
+
header=False if overwrite_option == "a" else header,
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
# This is a false positive on pylint and being tracked in pylint #3060
|
| 806 |
+
# pylint: disable=abstract-class-instantiated,too-many-positional-arguments
|
| 807 |
+
def export_data(
|
| 808 |
+
export_type: str,
|
| 809 |
+
dir_path: str,
|
| 810 |
+
func_name: str,
|
| 811 |
+
df: pd.DataFrame = pd.DataFrame(),
|
| 812 |
+
sheet_name: Optional[str] = None,
|
| 813 |
+
figure: Optional["OpenBBFigure"] = None,
|
| 814 |
+
margin: bool = True,
|
| 815 |
+
) -> None:
|
| 816 |
+
"""Export data to a file.
|
| 817 |
+
|
| 818 |
+
Parameters
|
| 819 |
+
----------
|
| 820 |
+
export_type : str
|
| 821 |
+
Type of export between: csv,json,xlsx,xls
|
| 822 |
+
dir_path : str
|
| 823 |
+
Path of directory from where this function is called
|
| 824 |
+
func_name : str
|
| 825 |
+
Name of the command that invokes this function
|
| 826 |
+
df : pd.Dataframe
|
| 827 |
+
Dataframe of data to save
|
| 828 |
+
sheet_name : str
|
| 829 |
+
If provided. The name of the sheet to save in excel file
|
| 830 |
+
figure : Optional[OpenBBFigure]
|
| 831 |
+
Figure object to save as image file
|
| 832 |
+
margin : bool
|
| 833 |
+
Automatically adjust subplot parameters to give specified padding.
|
| 834 |
+
"""
|
| 835 |
+
if export_type:
|
| 836 |
+
saved_path = compose_export_path(func_name, dir_path).resolve()
|
| 837 |
+
saved_path.parent.mkdir(parents=True, exist_ok=True)
|
| 838 |
+
for exp_type in export_type.split(","):
|
| 839 |
+
# In this scenario the path was provided, e.g. --export pt.csv, pt.jpg
|
| 840 |
+
if "." in exp_type:
|
| 841 |
+
saved_path = saved_path.with_name(exp_type)
|
| 842 |
+
# In this scenario we use the default filename
|
| 843 |
+
else:
|
| 844 |
+
if ".OpenBB_openbb_cli" in saved_path.name:
|
| 845 |
+
saved_path = saved_path.with_name(
|
| 846 |
+
saved_path.name.replace(".OpenBB_openbb_cli", "OpenBBCLI")
|
| 847 |
+
)
|
| 848 |
+
saved_path = saved_path.with_suffix(f".{exp_type}")
|
| 849 |
+
|
| 850 |
+
exists, overwrite = False, False
|
| 851 |
+
is_xlsx = exp_type.endswith("xlsx")
|
| 852 |
+
if sheet_name is None and is_xlsx or not is_xlsx:
|
| 853 |
+
exists, overwrite = ask_file_overwrite(saved_path)
|
| 854 |
+
|
| 855 |
+
if exists and not overwrite:
|
| 856 |
+
existing = len(list(saved_path.parent.glob(saved_path.stem + "*")))
|
| 857 |
+
saved_path = saved_path.with_stem(f"{saved_path.stem}_{existing + 1}")
|
| 858 |
+
|
| 859 |
+
df = df.replace(
|
| 860 |
+
{
|
| 861 |
+
r"\[yellow\]": "",
|
| 862 |
+
r"\[/yellow\]": "",
|
| 863 |
+
r"\[green\]": "",
|
| 864 |
+
r"\[/green\]": "",
|
| 865 |
+
r"\[red\]": "",
|
| 866 |
+
r"\[/red\]": "",
|
| 867 |
+
r"\[magenta\]": "",
|
| 868 |
+
r"\[/magenta\]": "",
|
| 869 |
+
},
|
| 870 |
+
regex=True,
|
| 871 |
+
)
|
| 872 |
+
|
| 873 |
+
if exp_type.endswith("csv"):
|
| 874 |
+
df.to_csv(saved_path)
|
| 875 |
+
elif exp_type.endswith("json"):
|
| 876 |
+
df.reset_index(drop=True, inplace=True)
|
| 877 |
+
df.to_json(saved_path)
|
| 878 |
+
elif exp_type.endswith("xlsx"):
|
| 879 |
+
# since xlsx does not support datetimes with timezones we need to remove it
|
| 880 |
+
df = remove_timezone_from_dataframe(df)
|
| 881 |
+
|
| 882 |
+
if sheet_name is None: # noqa: SIM223
|
| 883 |
+
df.to_excel(
|
| 884 |
+
saved_path,
|
| 885 |
+
index=True,
|
| 886 |
+
header=True,
|
| 887 |
+
)
|
| 888 |
+
else:
|
| 889 |
+
save_to_excel(df, saved_path, sheet_name)
|
| 890 |
+
|
| 891 |
+
elif saved_path.suffix in [".jpg", ".png"]:
|
| 892 |
+
if figure is None:
|
| 893 |
+
session.console.print("No plot to export.")
|
| 894 |
+
continue
|
| 895 |
+
figure.show(export_image=saved_path, margin=margin)
|
| 896 |
+
else:
|
| 897 |
+
session.console.print("Wrong export file specified.")
|
| 898 |
+
continue
|
| 899 |
+
|
| 900 |
+
if saved_path.exists():
|
| 901 |
+
session.console.print(f"Saved file: {saved_path}")
|
| 902 |
+
else:
|
| 903 |
+
session.console.print(f"Failed to save file: {saved_path}")
|
| 904 |
+
|
| 905 |
+
if figure is not None:
|
| 906 |
+
figure._exported = True # pylint: disable=protected-access
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
def system_clear():
|
| 910 |
+
"""Clear screen."""
|
| 911 |
+
os.system("cls||clear") # nosec # noqa: S605,S607
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
# Write an abstract helper to make requests from a url with potential headers and params
|
| 915 |
+
def request(
|
| 916 |
+
url: str, method: str = "get", timeout: int = 0, **kwargs
|
| 917 |
+
) -> requests.Response:
|
| 918 |
+
"""Make requests from a url with potential headers and params.
|
| 919 |
+
|
| 920 |
+
Parameters
|
| 921 |
+
----------
|
| 922 |
+
url : str
|
| 923 |
+
Url to make the request to
|
| 924 |
+
method : str
|
| 925 |
+
HTTP method to use. Choose from:
|
| 926 |
+
delete, get, head, patch, post, put, by default "get"
|
| 927 |
+
timeout : int
|
| 928 |
+
How many seconds to wait for the server to send data
|
| 929 |
+
|
| 930 |
+
Returns
|
| 931 |
+
-------
|
| 932 |
+
requests.Response
|
| 933 |
+
Request response object
|
| 934 |
+
|
| 935 |
+
Raises
|
| 936 |
+
------
|
| 937 |
+
ValueError
|
| 938 |
+
If invalid method is passed
|
| 939 |
+
"""
|
| 940 |
+
method = method.lower()
|
| 941 |
+
if method not in ["delete", "get", "head", "patch", "post", "put"]:
|
| 942 |
+
raise ValueError(f"Invalid method: {method}")
|
| 943 |
+
# We want to add a user agent to the request, so check if there are any headers
|
| 944 |
+
# If there are headers, check if there is a user agent, if not add one.
|
| 945 |
+
# Some requests seem to work only with a specific user agent, so we want to be able to override it.
|
| 946 |
+
headers = kwargs.pop("headers", {})
|
| 947 |
+
timeout = timeout or session.user.preferences.request_timeout
|
| 948 |
+
|
| 949 |
+
if "User-Agent" not in headers:
|
| 950 |
+
headers["User-Agent"] = get_user_agent()
|
| 951 |
+
func = getattr(requests, method)
|
| 952 |
+
return func(
|
| 953 |
+
url,
|
| 954 |
+
headers=headers,
|
| 955 |
+
timeout=timeout,
|
| 956 |
+
**kwargs,
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
|
| 960 |
+
def parse_unknown_args_to_dict(unknown_args: Optional[List[str]]) -> Dict[str, str]:
|
| 961 |
+
"""Parse unknown arguments to a dictionary."""
|
| 962 |
+
unknown_args_dict = {}
|
| 963 |
+
if unknown_args:
|
| 964 |
+
for idx, arg in enumerate(unknown_args):
|
| 965 |
+
if arg.startswith("--"):
|
| 966 |
+
if idx + 1 < len(unknown_args):
|
| 967 |
+
try:
|
| 968 |
+
unknown_args_dict[arg.replace("--", "")] = (
|
| 969 |
+
eval( # noqa: S307, E501 pylint: disable=eval-used
|
| 970 |
+
unknown_args[idx + 1]
|
| 971 |
+
)
|
| 972 |
+
)
|
| 973 |
+
except Exception:
|
| 974 |
+
unknown_args_dict[arg] = unknown_args[idx + 1]
|
| 975 |
+
else:
|
| 976 |
+
session.console.print(
|
| 977 |
+
f"Missing value for argument {arg}. Skipping this argument."
|
| 978 |
+
)
|
| 979 |
+
return unknown_args_dict
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
def handle_obbject_display(
|
| 983 |
+
obbject: OBBject,
|
| 984 |
+
chart: bool = False,
|
| 985 |
+
export: str = "",
|
| 986 |
+
sheet_name: str = "",
|
| 987 |
+
**kwargs,
|
| 988 |
+
):
|
| 989 |
+
"""Handle the display of an OBBject."""
|
| 990 |
+
df: pd.DataFrame = pd.DataFrame()
|
| 991 |
+
fig: Optional[OpenBBFigure] = None
|
| 992 |
+
if chart:
|
| 993 |
+
try:
|
| 994 |
+
if obbject.chart:
|
| 995 |
+
obbject.show(**kwargs)
|
| 996 |
+
else:
|
| 997 |
+
obbject.charting.to_chart(**kwargs) # type: ignore
|
| 998 |
+
if export:
|
| 999 |
+
fig = obbject.chart.fig # type: ignore
|
| 1000 |
+
df = obbject.to_dataframe()
|
| 1001 |
+
except Exception as e:
|
| 1002 |
+
session.console.print(f"Failed to display chart: {e}")
|
| 1003 |
+
elif session.settings.USE_INTERACTIVE_DF:
|
| 1004 |
+
obbject.charting.table() # type: ignore
|
| 1005 |
+
else:
|
| 1006 |
+
df = obbject.to_dataframe()
|
| 1007 |
+
print_rich_table(
|
| 1008 |
+
df=df,
|
| 1009 |
+
show_index=True,
|
| 1010 |
+
title=obbject.extra.get("command", ""),
|
| 1011 |
+
export=bool(export),
|
| 1012 |
+
)
|
| 1013 |
+
if export and not df.empty:
|
| 1014 |
+
if sheet_name and isinstance(sheet_name, list):
|
| 1015 |
+
sheet_name = sheet_name[0]
|
| 1016 |
+
|
| 1017 |
+
func_name = (
|
| 1018 |
+
obbject.extra.get("command", "")
|
| 1019 |
+
.replace("/", "_")
|
| 1020 |
+
.replace(" ", "_")
|
| 1021 |
+
.replace("--", "_")
|
| 1022 |
+
)
|
| 1023 |
+
export_data(
|
| 1024 |
+
export_type=",".join(export),
|
| 1025 |
+
dir_path=os.path.dirname(os.path.abspath(__file__)),
|
| 1026 |
+
func_name=func_name,
|
| 1027 |
+
df=df,
|
| 1028 |
+
sheet_name=sheet_name,
|
| 1029 |
+
figure=fig,
|
| 1030 |
+
)
|
| 1031 |
+
elif export and df.empty:
|
| 1032 |
+
session.console.print("[yellow]No data to export.[/yellow]")
|
cli/openbb_cli/models/settings.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Settings model."""
|
| 2 |
+
|
| 3 |
+
from enum import Enum
|
| 4 |
+
from typing import Any, Literal
|
| 5 |
+
|
| 6 |
+
from dotenv import dotenv_values, set_key
|
| 7 |
+
from openbb_cli.config.constants import AVAILABLE_FLAIRS, ENV_FILE_SETTINGS
|
| 8 |
+
from openbb_core.app.version import get_package_version
|
| 9 |
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
| 10 |
+
from pytz import all_timezones
|
| 11 |
+
|
| 12 |
+
VERSION = get_package_version("openbb-cli")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class SettingGroups(Enum):
|
| 16 |
+
"""Setting types."""
|
| 17 |
+
|
| 18 |
+
feature_flags = "feature_flag"
|
| 19 |
+
preferences = "preference"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Settings(BaseModel):
|
| 23 |
+
"""Settings model."""
|
| 24 |
+
|
| 25 |
+
# Platform CLI version
|
| 26 |
+
VERSION: str = VERSION
|
| 27 |
+
|
| 28 |
+
# DEVELOPMENT FLAGS
|
| 29 |
+
TEST_MODE: bool = False
|
| 30 |
+
DEBUG_MODE: bool = False
|
| 31 |
+
DEV_BACKEND: bool = False
|
| 32 |
+
|
| 33 |
+
# OPENBB
|
| 34 |
+
HUB_URL: str = "https://my.openbb.co"
|
| 35 |
+
BASE_URL: str = "https://payments.openbb.co"
|
| 36 |
+
|
| 37 |
+
# GENERAL
|
| 38 |
+
PREVIOUS_USE: bool = False
|
| 39 |
+
|
| 40 |
+
# FEATURE FLAGS
|
| 41 |
+
FILE_OVERWRITE: bool = Field(
|
| 42 |
+
default=False,
|
| 43 |
+
description="whether to overwrite Excel files if they already exists",
|
| 44 |
+
command="overwrite",
|
| 45 |
+
group=SettingGroups.feature_flags,
|
| 46 |
+
)
|
| 47 |
+
SHOW_VERSION: bool = Field(
|
| 48 |
+
default=True,
|
| 49 |
+
description="whether to show the version in the bottom right corner",
|
| 50 |
+
command="version",
|
| 51 |
+
group=SettingGroups.feature_flags,
|
| 52 |
+
)
|
| 53 |
+
USE_INTERACTIVE_DF: bool = Field(
|
| 54 |
+
default=True,
|
| 55 |
+
description="display tables in interactive window",
|
| 56 |
+
command="interactive",
|
| 57 |
+
group=SettingGroups.feature_flags,
|
| 58 |
+
)
|
| 59 |
+
USE_CLEAR_AFTER_CMD: bool = Field(
|
| 60 |
+
default=False,
|
| 61 |
+
description="clear console after each command",
|
| 62 |
+
command="cls",
|
| 63 |
+
group=SettingGroups.feature_flags,
|
| 64 |
+
)
|
| 65 |
+
USE_DATETIME: bool = Field(
|
| 66 |
+
default=True,
|
| 67 |
+
description="whether to show the date and time before the flair",
|
| 68 |
+
command="datetime",
|
| 69 |
+
group=SettingGroups.feature_flags,
|
| 70 |
+
)
|
| 71 |
+
USE_PROMPT_TOOLKIT: bool = Field(
|
| 72 |
+
default=True,
|
| 73 |
+
description="enable prompt toolkit (autocomplete and history)",
|
| 74 |
+
command="promptkit",
|
| 75 |
+
group=SettingGroups.feature_flags,
|
| 76 |
+
)
|
| 77 |
+
ENABLE_EXIT_AUTO_HELP: bool = Field(
|
| 78 |
+
default=True,
|
| 79 |
+
description="automatically print help when quitting menu",
|
| 80 |
+
command="exithelp",
|
| 81 |
+
group=SettingGroups.feature_flags,
|
| 82 |
+
)
|
| 83 |
+
ENABLE_RICH_PANEL: bool = Field(
|
| 84 |
+
default=True,
|
| 85 |
+
description="enable colorful rich CLI panel",
|
| 86 |
+
command="richpanel",
|
| 87 |
+
group=SettingGroups.feature_flags,
|
| 88 |
+
)
|
| 89 |
+
TOOLBAR_HINT: bool = Field(
|
| 90 |
+
default=True,
|
| 91 |
+
description="displays usage hints in the bottom toolbar",
|
| 92 |
+
command="tbhint",
|
| 93 |
+
group=SettingGroups.feature_flags,
|
| 94 |
+
)
|
| 95 |
+
SHOW_MSG_OBBJECT_REGISTRY: bool = Field(
|
| 96 |
+
default=False,
|
| 97 |
+
description="show obbject registry message after a new result is added",
|
| 98 |
+
command="obbject_msg",
|
| 99 |
+
group=SettingGroups.feature_flags,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# PREFERENCES
|
| 103 |
+
TIMEZONE: Literal[tuple(all_timezones)] = Field( # type: ignore[valid-type]
|
| 104 |
+
default="America/New_York",
|
| 105 |
+
description="pick timezone",
|
| 106 |
+
command="timezone",
|
| 107 |
+
group=SettingGroups.preferences,
|
| 108 |
+
)
|
| 109 |
+
FLAIR: Literal[tuple(AVAILABLE_FLAIRS)] = Field( # type: ignore[valid-type]
|
| 110 |
+
default=":openbb",
|
| 111 |
+
description="choose flair icon",
|
| 112 |
+
command="flair",
|
| 113 |
+
group=SettingGroups.preferences,
|
| 114 |
+
)
|
| 115 |
+
N_TO_KEEP_OBBJECT_REGISTRY: int = Field(
|
| 116 |
+
default=10,
|
| 117 |
+
description="define the maximum number of obbjects allowed in the registry",
|
| 118 |
+
command="obbject_res",
|
| 119 |
+
group=SettingGroups.preferences,
|
| 120 |
+
)
|
| 121 |
+
N_TO_DISPLAY_OBBJECT_REGISTRY: int = Field(
|
| 122 |
+
default=5,
|
| 123 |
+
description="define the maximum number of cached results to display on the help menu",
|
| 124 |
+
command="obbject_display",
|
| 125 |
+
group=SettingGroups.preferences,
|
| 126 |
+
)
|
| 127 |
+
RICH_STYLE: str = Field(
|
| 128 |
+
default="dark",
|
| 129 |
+
description="apply a custom rich style to the CLI",
|
| 130 |
+
command="console_style",
|
| 131 |
+
group=SettingGroups.preferences,
|
| 132 |
+
)
|
| 133 |
+
ALLOWED_NUMBER_OF_ROWS: int = Field(
|
| 134 |
+
default=20,
|
| 135 |
+
description="number of rows to show (when not using interactive tables).",
|
| 136 |
+
command="n_rows",
|
| 137 |
+
group=SettingGroups.preferences,
|
| 138 |
+
)
|
| 139 |
+
ALLOWED_NUMBER_OF_COLUMNS: int = Field(
|
| 140 |
+
default=5,
|
| 141 |
+
description="number of columns to show (when not using interactive tables).",
|
| 142 |
+
command="n_cols",
|
| 143 |
+
group=SettingGroups.preferences,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
model_config = ConfigDict(validate_assignment=True)
|
| 147 |
+
|
| 148 |
+
def __repr__(self) -> str:
|
| 149 |
+
"""Return a string representation of the model."""
|
| 150 |
+
return f"{self.__class__.__name__}\n\n" + "\n".join(
|
| 151 |
+
f"{k}: {v}" for k, v in self.model_dump().items()
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
@model_validator(mode="before")
|
| 155 |
+
@classmethod
|
| 156 |
+
def from_env(cls, values: dict) -> dict:
|
| 157 |
+
"""Load settings from .env."""
|
| 158 |
+
settings = {}
|
| 159 |
+
settings.update(dotenv_values(ENV_FILE_SETTINGS))
|
| 160 |
+
settings.update(values)
|
| 161 |
+
filtered = {k.replace("OPENBB_", ""): v for k, v in settings.items()}
|
| 162 |
+
return filtered
|
| 163 |
+
|
| 164 |
+
def set_item(self, key: str, value: Any) -> None:
|
| 165 |
+
"""Set an item in the model and save to .env."""
|
| 166 |
+
setattr(self, key, value)
|
| 167 |
+
set_key(str(ENV_FILE_SETTINGS), "OPENBB_" + key, str(value))
|
cli/openbb_cli/session.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Settings module."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
from openbb import obb
|
| 8 |
+
from openbb_charting.core.backend import create_backend, get_backend
|
| 9 |
+
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
| 10 |
+
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
| 11 |
+
from openbb_core.app.model.user_settings import UserSettings as User
|
| 12 |
+
from prompt_toolkit import PromptSession
|
| 13 |
+
|
| 14 |
+
from openbb_cli.argparse_translator.obbject_registry import Registry
|
| 15 |
+
from openbb_cli.config.completer import CustomFileHistory
|
| 16 |
+
from openbb_cli.config.console import Console
|
| 17 |
+
from openbb_cli.config.constants import HIST_FILE_PROMPT
|
| 18 |
+
from openbb_cli.config.style import Style
|
| 19 |
+
from openbb_cli.models.settings import Settings
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _get_backend():
|
| 23 |
+
"""Get the Platform charting backend."""
|
| 24 |
+
try:
|
| 25 |
+
return get_backend()
|
| 26 |
+
except ValueError:
|
| 27 |
+
# backend might not be created yet
|
| 28 |
+
charting_settings = ChartingSettings(
|
| 29 |
+
system_settings=obb.system, user_settings=obb.user # type: ignore
|
| 30 |
+
)
|
| 31 |
+
create_backend(charting_settings)
|
| 32 |
+
get_backend().start(debug=charting_settings.debug_mode) # type: ignore
|
| 33 |
+
return get_backend()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class Session(metaclass=SingletonMeta):
|
| 37 |
+
"""Session class."""
|
| 38 |
+
|
| 39 |
+
def __init__(self):
|
| 40 |
+
"""Initialize session."""
|
| 41 |
+
|
| 42 |
+
self._obb = obb
|
| 43 |
+
self._settings = Settings()
|
| 44 |
+
self._style = Style(
|
| 45 |
+
style=self._settings.RICH_STYLE,
|
| 46 |
+
directory=Path(self._obb.user.preferences.user_styles_directory), # type: ignore[union-attr]
|
| 47 |
+
)
|
| 48 |
+
self._console = Console(
|
| 49 |
+
settings=self._settings, style=self._style.console_style
|
| 50 |
+
)
|
| 51 |
+
self._prompt_session = self._get_prompt_session()
|
| 52 |
+
self._obbject_registry = Registry()
|
| 53 |
+
|
| 54 |
+
self._backend = _get_backend()
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def user(self) -> User:
|
| 58 |
+
"""Get platform user."""
|
| 59 |
+
return self._obb.user # type: ignore[union-attr]
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def settings(self) -> Settings:
|
| 63 |
+
"""Get CLI settings."""
|
| 64 |
+
return self._settings
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def style(self) -> Style:
|
| 68 |
+
"""Get CLI style."""
|
| 69 |
+
return self._style
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def console(self) -> Console:
|
| 73 |
+
"""Get console."""
|
| 74 |
+
return self._console
|
| 75 |
+
|
| 76 |
+
@property
|
| 77 |
+
def obbject_registry(self) -> Registry:
|
| 78 |
+
"""Get obbject registry."""
|
| 79 |
+
return self._obbject_registry
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def prompt_session(self) -> Optional[PromptSession]:
|
| 83 |
+
"""Get prompt session."""
|
| 84 |
+
return self._prompt_session
|
| 85 |
+
|
| 86 |
+
def _get_prompt_session(self) -> Optional[PromptSession]:
|
| 87 |
+
"""Initialize prompt session."""
|
| 88 |
+
try:
|
| 89 |
+
if sys.stdin.isatty():
|
| 90 |
+
prompt_session: Optional[PromptSession] = PromptSession(
|
| 91 |
+
history=CustomFileHistory(str(HIST_FILE_PROMPT))
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
prompt_session = None
|
| 95 |
+
except Exception:
|
| 96 |
+
prompt_session = None
|
| 97 |
+
|
| 98 |
+
return prompt_session
|
| 99 |
+
|
| 100 |
+
def is_local(self) -> bool:
|
| 101 |
+
"""Check if user is local."""
|
| 102 |
+
return not bool(self.user.profile.hub_session)
|
| 103 |
+
|
| 104 |
+
def max_obbjects_exceeded(self) -> bool:
|
| 105 |
+
"""Check if max obbjects exceeded."""
|
| 106 |
+
return (
|
| 107 |
+
len(self.obbject_registry.all) >= self.settings.N_TO_KEEP_OBBJECT_REGISTRY
|
| 108 |
+
)
|
cli/openbb_cli/utils/utils.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Platform CLI utilities."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
HOME_DIRECTORY = Path.home()
|
| 7 |
+
OPENBB_PLATFORM_DIRECTORY = Path(HOME_DIRECTORY, ".openbb_platform")
|
| 8 |
+
SYSTEM_SETTINGS_PATH = Path(OPENBB_PLATFORM_DIRECTORY, "system_settings.json")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def change_logging_sub_app() -> str:
|
| 12 |
+
"""Build OpenBB Platform setting files."""
|
| 13 |
+
with open(SYSTEM_SETTINGS_PATH) as file:
|
| 14 |
+
system_settings = json.load(file)
|
| 15 |
+
|
| 16 |
+
initial_logging_sub_app = system_settings.get("logging_sub_app", "")
|
| 17 |
+
|
| 18 |
+
system_settings["logging_sub_app"] = "cli"
|
| 19 |
+
|
| 20 |
+
with open(SYSTEM_SETTINGS_PATH, "w") as file:
|
| 21 |
+
json.dump(system_settings, file, indent=4)
|
| 22 |
+
|
| 23 |
+
return initial_logging_sub_app
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def reset_logging_sub_app(initial_logging_sub_app: str):
|
| 27 |
+
"""Reset OpenBB Platform setting files."""
|
| 28 |
+
with open(SYSTEM_SETTINGS_PATH) as file:
|
| 29 |
+
system_settings = json.load(file)
|
| 30 |
+
|
| 31 |
+
system_settings["logging_sub_app"] = initial_logging_sub_app
|
| 32 |
+
|
| 33 |
+
with open(SYSTEM_SETTINGS_PATH, "w") as file:
|
| 34 |
+
json.dump(system_settings, file, indent=4)
|