summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-06-26 07:51:12 -0700
committerGitHub <noreply@github.com>2024-06-26 07:51:12 -0700
commita0538d398c7704f64b338d59d08e999f961851a4 (patch)
tree8b623855970e8dff13611bbde2e530d41c2b5be8
parentf532097276cf57589953fbfe935caecb286d8e50 (diff)
parent34122c4524df379d1b17ccd9c1bbae6455e046ba (diff)
Merge branch 'develop' into feature/delay-import
-rw-r--r--cli/integration/test_commands.py29
-rw-r--r--cli/integration/test_integration_base_controller.py89
-rw-r--r--cli/integration/test_integration_base_platform_controller.py81
-rw-r--r--cli/integration/test_integration_cli_controller.py26
-rw-r--r--cli/integration/test_integration_hub_service.py62
-rw-r--r--cli/integration/test_integration_obbject_registry.py55
-rw-r--r--cli/openbb_cli/controllers/base_controller.py93
-rw-r--r--cli/openbb_cli/controllers/base_platform_controller.py4
-rw-r--r--cli/openbb_cli/controllers/choices.py7
-rw-r--r--cli/openbb_cli/controllers/cli_controller.py3
-rw-r--r--cli/openbb_cli/controllers/settings_controller.py4
-rw-r--r--cli/openbb_cli/controllers/utils.py74
-rw-r--r--openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_views.py17
-rw-r--r--openbb_platform/extensions/derivatives/pyproject.toml3
-rw-r--r--openbb_platform/obbject_extensions/charting/integration/test_charting_api.py33
-rw-r--r--openbb_platform/obbject_extensions/charting/integration/test_charting_python.py25
-rw-r--r--openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py3
17 files changed, 557 insertions, 51 deletions
diff --git a/cli/integration/test_commands.py b/cli/integration/test_commands.py
new file mode 100644
index 00000000000..517acd28eaa
--- /dev/null
+++ b/cli/integration/test_commands.py
@@ -0,0 +1,29 @@
+import io
+
+import pytest
+from openbb_cli.cli import main
+
+
+@pytest.mark.parametrize(
+ "input_values",
+ [
+ "/equity/price/historical --symbol aapl --provider fmp",
+ "/equity/price/historical --symbol msft --provider yfinance",
+ "/equity/price/historical --symbol goog --provider polygon",
+ "/crypto/price/historical --symbol btc --provider fmp",
+ "/currency/price/historical --symbol eur --provider fmp",
+ "/derivatives/futures/historical --symbol cl --provider fmp",
+ "/etf/price/historical --symbol spy --provider fmp",
+ "/economy",
+ ],
+)
+@pytest.mark.integration
+def test_launch_with_cli_input(monkeypatch, input_values):
+ """Test launching the CLI and providing input via stdin with multiple parameters."""
+ stdin = io.StringIO(input_values)
+ monkeypatch.setattr("sys.stdin", stdin)
+
+ try:
+ main()
+ except Exception as e:
+ pytest.fail(f"Main function raised an exception: {e}")
diff --git a/cli/integration/test_integration_base_controller.py b/cli/integration/test_integration_base_controller.py
new file mode 100644
index 00000000000..efab88d1b30
--- /dev/null
+++ b/cli/integration/test_integration_base_controller.py
@@ -0,0 +1,89 @@
+"""Integration tests for the base_controller module."""
+
+from unittest.mock import Mock, patch
+
+import pytest
+from openbb_cli.controllers.base_controller import BaseController
+from openbb_cli.session import Session
+
+# pylint: disable=unused-variable, redefined-outer-name
+
+
+class TestController(BaseController):
+ """Test controller for the BaseController."""
+
+ PATH = "/test/"
+
+ def print_help(self):
+ """Print help message."""
+
+
+@pytest.fixture
+def base_controller():
+ """Set up the environment for each test function."""
+ session = Session() # noqa: F841
+ controller = TestController()
+ return controller
+
+
+@pytest.mark.integration
+def test_check_path_valid(base_controller):
+ """Test that check_path does not raise an error for a valid path."""
+ base_controller.PATH = "/equity/"
+ try:
+ base_controller.check_path()
+ except ValueError:
+ pytest.fail("check_path raised ValueError unexpectedly!")
+
+
+@pytest.mark.integration
+def test_check_path_invalid(base_controller):
+ """Test that check_path raises an error for an invalid path."""
+ with pytest.raises(ValueError):
+ base_controller.PATH = "invalid_path" # Missing leading '/'
+ base_controller.check_path()
+
+ with pytest.raises(ValueError):
+ base_controller.PATH = "/invalid_path" # Missing trailing '/'
+ base_controller.check_path()
+
+
+@pytest.mark.integration
+def test_parse_input(base_controller):
+ """Test the parse_input method."""
+ input_str = "/equity/price/help"
+ expected_output = ["", "equity", "price", "help"]
+ assert (
+ base_controller.parse_input(input_str) == expected_output
+ ), "Input parsing failed"
+
+
+@pytest.mark.integration
+def test_switch_command_execution(base_controller):
+ """Test the switch method."""
+ base_controller.queue = []
+ base_controller.switch("/home/../reset/")
+ assert base_controller.queue == [
+ "home",
+ "..",
+ "reset",
+ ], "Switch did not update the queue correctly"
+
+
+@patch("openbb_cli.controllers.base_controller.BaseController.call_help")
+@pytest.mark.integration
+def test_command_routing(mock_call_help, base_controller):
+ """Test the command routing."""
+ base_controller.switch("help")
+ mock_call_help.assert_called_once()
+
+
+@pytest.mark.integration
+def test_custom_reset(base_controller):
+ """Test the custom reset method."""
+ base_controller.custom_reset = Mock(return_value=["custom", "reset"])
+ base_controller.call_reset(None)
+ expected_queue = ["quit", "reset", "custom", "reset"]
+ assert (
+ base_controller.queue == expected_queue
+ ), f"Expected queue to be {expected_queue}, but was {base_controller.queue}"
diff --git a/cli/integration/test_integration_base_platform_controller.py b/cli/integration/test_integration_base_platform_controller.py
new file mode 100644
index 00000000000..2d645f14129
--- /dev/null
+++ b/cli/integration/test_integration_base_platform_controller.py
@@ -0,0 +1,81 @@
+"""Test the base platform controller."""
+
+from unittest.mock import MagicMock, Mock, patch
+
+import pytest
+from openbb_cli.controllers.base_platform_controller import (
+ PlatformController,
+ Session,
+)
+
+# pylint: disable=protected-access, unused-variable, redefined-outer-name
+
+
+@pytest.fixture
+def platform_controller():
+ """Return a platform controller."""
+ session = Session() # noqa: F841
+ translators = {"test_command": MagicMock(), "test_menu": MagicMock()} # noqa: F841
+ translators["test_command"]._parser = Mock(
+ _actions=[Mock(dest="data", choices=[], type=str, nargs=None)]
+ )
+ translators["test_command"].execute_func = Mock(return_value=Mock())
+ translators["test_menu"]._parser = Mock(
+ _actions=[Mock(dest="data", choices=[], type=str, nargs=None)]
+ )
+ translators["test_menu"].execute_func = Mock(return_value=Mock())
+
+ controller = PlatformController(
+ name="test", parent_path=["platform"], translators=translators
+ )
+ return controller
+
+
+@pytest.mark.integration
+def test_platform_controller_initialization(platform_controller):
+ """Test the initialization of the platform controller."""
+ expected_path = "/platform/test/"
+ assert (
+ expected_path == platform_controller.PATH
+ ), "Controller path was not set correctly"
+
+
+@pytest.mark.integration
+def test_command_generation(platform_controller):
+ """Test the generation of commands."""
+ command_name = "test_command"
+ mock_execute_func = Mock(return_value=(Mock(), None))
+ platform_controller.translators[command_name].execute_func = mock_execute_func
+
+ platform_controller._generate_command_call(
+ name=command_name, translator=platform_controller.translators[command_name]
+ )
+ command_method_name = f"call_{command_name}"
+ assert hasattr(
+ platform_controller, command_method_name
+ ), "Command method was not created"
+
+
+@patch(
+ "openbb_cli.controllers.base_platform_controller.PlatformController._link_obbject_to_data_processing_commands"
+)
+@patch(
+ "openbb_cli.controllers.base_platform_controller.PlatformController._generate_commands"
+)
+@patch(
+ "openbb_cli.controllers.base_platform_controller.PlatformController._generate_sub_controllers"
+)
+@pytest.mark.integration
+def test_platform_controller_calls(
+ mock_sub_controllers, mock_commands, mock_link_commands
+):
+ """Test the calls of the platform controller."""
+ translators = {"test_command": Mock()}
+ translators["test_command"].parser = Mock()
+ translators["test_command"].execute_func = Mock()
+ _ = PlatformController(
+ name="test", parent_path=["platform"], translators=translators
+ )
+ mock_sub_controllers.assert_called_once()
+ mock_commands.assert_called_once()
+ mock_link_commands.assert_called_once()
diff --git a/cli/integration/test_integration_cli_controller.py b/cli/integration/test_integration_cli_controller.py
new file mode 100644
index 00000000000..8b76a8499f3
--- /dev/null
+++ b/cli/integration/test_integration_cli_controller.py
@@ -0,0 +1,26 @@
+"""Test the CLI controller integration."""
+
+from openbb_cli.controllers.cli_controller import (
+ CLIController,
+)
+
+
+def test_parse_input_valid_commands():
+ """Test parse_input method."""
+ controller = CLIController()
+ input_string = "exe --file test.openbb"
+ expected_output = [
+ "exe --file test.openbb"
+ ] # Adjust based on actual expected behavior
+ assert controller.parse_input(input_string) == expected_output
+
+
+def test_parse_input_invalid_commands():
+ """Test parse_input method."""
+ controller = CLIController()
+ input_string = "nonexistentcommand args"
+ expected_output = ["nonexistentcommand args"]
+ actual_output = controller.parse_input(input_string)
+ assert (
+ actual_output == expected_output
+ ), f"Expected {expected_output}, got {actual_output}"
diff --git a/cli/integration/test_integration_hub_service.py b/cli/integration/test_integration_hub_service.py
new file mode 100644
index 00000000000..9413372b786
--- /dev/null
+++ b/cli/integration/test_integration_hub_service.py
@@ -0,0 +1,62 @@
+"""Integration tests for the hub_service module."""
+
+from unittest.mock import create_autospec, patch
+
+import pytest
+import requests
+from openbb_cli.controllers.hub_service import upload_routine
+from openbb_core.app.model.hub.hub_session import HubSession
+
+# pylint: disable=unused-argument, redefined-outer-name, unused-variable
+
+
+@pytest.fixture
+def auth_header():
+ """Return a fake auth header."""
+ return "Bearer fake_token"
+
+
+@pytest.fixture
+def hub_session_mock():
+ """Return a mock HubSession."""
+ mock = create_autospec(HubSession, instance=True)
+ mock.username = "TestUser"
+ return mock
+
+
+# Fixture for routine data
+@pytest.fixture
+def routine_data():
+ """Return a dictionary with routine data."""
+ return {
+ "name": "Test Routine",
+ "description": "A test routine",
+ "routine": "print('Hello World')",
+ "override": False,
+ "tags": "test",
+ "public": True,
+ }
+
+
+@pytest.mark.integration
+def test_upload_routine_timeout(auth_header, routine_data):
+ """Test upload_routine with a timeout exception."""
+ with patch(
+ "requests.post", side_effect=requests.exceptions.Timeout
+ ) as mocked_post: # noqa: F841
+
+ response = upload_routine(auth_header, **routine_data)
+
+ assert response is None
+
+
+@pytest.mark.integration
+def test_upload_routine_connection_error(auth_header, routine_data):
+ """Test upload_routine with a connection error."""
+ with patch(
+ "requests.post", side_effect=requests.exceptions.ConnectionError
+ ) as mocked_post: # noqa: F841
+
+ response = upload_routine(auth_header, **routine_data)
+
+ assert response is None
diff --git a/cli/integration/test_integration_obbject_registry.py b/cli/integration/test_integration_obbject_registry.py
new file mode 100644
index 00000000000..ec12dd0e159
--- /dev/null
+++ b/cli/integration/test_integration_obbject_registry.py
@@ -0,0 +1,55 @@
+"""Test the obbject registry."""
+
+import pytest
+from openbb_cli.argparse_translator.obbject_registry import Registry
+from openbb_core.app.model.obbject import OBBject
+
+# pylint: disable=unused-variable
+# ruff: noqa: disable=F841
+
+
+def test_registry_operations():
+ """Test the registry operations."""
+ registry = Registry()
+ obbject1 = OBBject(
+ id="1", results=True, extra={"register_key": "key1", "command": "cmd1"}
+ )
+ obbject2 = OBBject(
+ id="2", results=True, extra={"register_key": "key2", "command": "cmd2"}
+ )
+ obbject3 = OBBject( # noqa: F841
+ id="3", results=True, extra={"register_key": "key3", "command": "cmd3"}
+ )
+
+ # Add obbjects to the registry
+ assert registry.register(obbject1) is True
+ assert registry.register(obbject2) is True
+ # Attempt to add the same object again
+ assert registry.register(obbject1) is False
+ # Ensure the registry size is correct
+ assert len(registry.obbjects) == 2
+
+ # Get by index
+ assert registry.get(0) == obbject2
+ assert registry.get(1) == obbject1
+ # Get by key
+ assert registry.get("key1") == obbject1
+ assert registry.get("key2") == obbject2
+ # Invalid index/key
+ assert registry.get(2) is None
+ assert registry.get("invalid_key") is None
+
+ # Remove an object
+ registry.remove(0)
+ assert len(registry.obbjects) == 1
+ assert registry.get("key2") is None
+
+ # Validate the 'all' property
+ all_obbjects = registry.all
+ assert "command" in all_obbjects[0]
+ assert all_obbjects[0]["command"] == "cmd1"
+
+ # Clean up by removing all objects
+ registry.remove()
+ assert len(registry.obbjects) == 0
+ assert registry.get("key1") is None
diff --git a/cli/openbb_cli/controllers/base_controller.py b/cli/openbb_cli/controllers/base_controller.py
index 91a353f8b60..da54491c11e 100644
--- a/cli/openbb_cli/controllers/base_controller.py
+++ b/cli/openbb_cli/controllers/base_controller.py
@@ -8,7 +8,7 @@ import shlex
from abc import ABCMeta, abstractmethod
from datetime import datetime
from pathlib import Path
-from typing import Any, Dict, List, Literal, Optional, Union
+from typing import Any, Dict, List, Literal, Optional, Tuple, Union
import pandas as pd
from openbb_cli.config.completer import NestedCompleter
@@ -19,7 +19,9 @@ from openbb_cli.controllers.utils import (
check_file_type_saved,
check_positive,
get_flair_and_username,
+ handle_obbject_display,
parse_and_split_input,
+ parse_unknown_args_to_dict,
print_guest_block_msg,
print_rich_table,
remove_file,
@@ -373,7 +375,7 @@ class BaseController(metaclass=ABCMeta):
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
- ns_parser = self.parse_simple_args(parser, other_args)
+ ns_parser, _ = self.parse_simple_args(parser, other_args)
if ns_parser:
if not ns_parser.name:
@@ -472,7 +474,7 @@ class BaseController(metaclass=ABCMeta):
description="Stop recording session into .openbb routine file",
)
# This is only for auto-completion purposes
- _ = self.parse_simple_args(parser, other_args)
+ _, _ = self.parse_simple_args(parser, other_args)
if "-h" not in other_args and "--help" not in other_args:
global RECORD_SESSION # noqa: PLW0603
@@ -603,7 +605,7 @@ class BaseController(metaclass=ABCMeta):
prog="whoami",
description="Show current user",
)
- ns_parser = self.parse_simple_args(parser, other_args)
+ ns_parser, _ = self.parse_simple_args(parser, other_args)
if ns_parser:
current_user = session.user
@@ -635,11 +637,27 @@ class BaseController(metaclass=ABCMeta):
"--chart", action="store_true", dest="chart", help="Display chart."
)
parser.add_argument(
- "--export", dest="export", help="Export data.", nargs="+", default=None
+ "--export",
+ default="",
+ type=check_file_type_saved(["csv", "json", "xlsx", "png", "jpg"]),
+ dest="export",
+ help="Export raw data into csv, json, xlsx and figure into png or jpg.",
+ nargs="+",
+ )
+ parser.add_argument(
+ "--sheet-name",
+ dest="sheet_name",
+ default=None,
+ nargs="+",
+ help="Name of excel sheet to save data to. Only valid for .xlsx files.",
+ )
+
+ ns_parser, unknown_args = self.parse_simple_args(
+ parser, other_args, unknown_args=True
)
- ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser:
+ kwargs = parse_unknown_args_to_dict(unknown_args)
if not ns_parser.index and not ns_parser.key:
results = session.obbject_registry.all
if results:
@@ -657,21 +675,13 @@ class BaseController(metaclass=ABCMeta):
index = int(ns_parser.index)
obbject = session.obbject_registry.get(index)
if obbject:
- if ns_parser.chart and obbject.chart:
- obbject.show()
- else:
- title = obbject.extra.get("command", "")
- df = obbject.to_dataframe()
- print_rich_table(
- df=df,
- show_index=True,
- title=title,
- export=ns_parser.export,
- )
- if ns_parser.chart and not obbject.chart:
- session.console.print(
- "[info]No chart available.[/info]"
- )
+ handle_obbject_display(
+ obbject=obbject,
+ chart=ns_parser.chart,
+ export=ns_parser.export,
+ sheet_name=ns_parser.sheet_name,
+ **kwargs,
+ )
else:
session.console.print(
f"[info]No result found at index {index}.[/info]"
@@ -683,26 +693,24 @@ class BaseController(metaclass=ABCMeta):
elif ns_parser.key:
obbject = session.obbject_registry.get(ns_parser.key)
if obbject:
- if ns_parser.chart and obbject.chart:
- obbject.show()
- else:
- title = obbject.extra.get("command", "")
- df = obbject.to_dataframe()
- print_rich_table(
- df=df,
- show_index=True,
- title=title,
- export=ns_parser.export,
- )
- if ns_parser.chart and not obbject.chart:
- session.console.print("[info]No chart available.[/info]")
+ handle_obbject_display(
+ obbject=obbject,
+ chart=ns_parser.chart,
+ export=ns_parser.export,
+ sheet_name=ns_parser.sheet_name,
+ **kwargs,
+ )
else:
session.console.print(
f"[info]No result found with key '{ns_parser.key}'.[/info]"
)
@staticmethod
- def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]):
+ def parse_simple_args(
+ parser: argparse.ArgumentParser,
+ other_args: List[str],
+ unknown_args: bool = False,
+ ) -> Tuple[Optional[argparse.Namespace], Optional[List[str]]]:
"""Parse list of arguments into the supplied parser.
Parameters
@@ -711,11 +719,15 @@ class BaseController(metaclass=ABCMeta):
Parser with predefined arguments
other_args: List[str]
List of arguments to parse
+ unknown_args: bool
+ Flag to indicate if unknown arguments should be returned
Returns
-------
- ns_parser:
+ ns_parser: argparse.Namespace
Namespace with parsed arguments
+ l_unknown_args: List[str]
+ List of unknown arguments
"""
parser.add_argument(
"-h", "--help", action="store_true", help="show this help message"
@@ -729,19 +741,18 @@ class BaseController(metaclass=ABCMeta):
except SystemExit:
# In case the command has required argument that isn't specified
session.console.print("\n")
- return None
+ return None, None
if ns_parser.help:
txt_help = parser.format_help()
session.console.print(f"[help]{txt_help}[/help]")
- return None
+ return None, None
- if l_unknown_args:
+ if l_unknown_args and not unknown_args:
session.console.print(
f"The following args couldn't be interpreted: {l_unknown_args}\n"
)
-
- return ns_parser
+ return ns_parser, l_unknown_args
@classmethod
def parse_known_args_and_warn(
diff --git a/cli/openbb_cli/controllers/base_platform_controller.py b/cli/openbb_cli/controllers/base_platform_controller.py
index a3cd0d1527c..d8363bec9b5 100644
--- a/cli/openbb_cli/controllers/base_platform_controller.py
+++ b/cli/openbb_cli/controllers/base_platform_controller.py
@@ -162,7 +162,7 @@ class PlatformController(BaseController):
):
try:
ns_parser = self._intersect_data_processing_commands(ns_parser)
-
+ export = hasattr(ns_parser, "export") and ns_parser.export
store_obbject = (
hasattr(ns_parser, "register_obbject")
and ns_parser.register_obbject
@@ -228,8 +228,6 @@ class PlatformController(BaseController):
# making the dataframe available either for printing or exporting
df = obbject.to_dataframe()
- export = hasattr(ns_parser, "export") and ns_parser.export
-
if hasattr(ns_parser, "chart") and ns_parser.chart:
fig = obbject.chart.fig if obbject.chart else None
if not export:
diff --git a/cli/openbb_cli/controllers/choices.py b/cli/openbb_cli/controllers/choices.py
index 3afc5234c11..f4e87552095 100644
--- a/cli/openbb_cli/controllers/choices.py
+++ b/cli/openbb_cli/controllers/choices.py
@@ -4,7 +4,7 @@ from argparse import SUPPRESS, ArgumentParser
from contextlib import contextmanager
from inspect import isfunction, unwrap
from types import MethodType
-from typing import Callable, List, Literal
+from typing import Callable, List, Literal, Tuple
from unittest.mock import patch
from openbb_cli.controllers.utils import (
@@ -110,7 +110,7 @@ def __mock_parse_known_args_and_warn(
)
-def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> None:
+def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> Tuple:
"""Add arguments.
Add the arguments that would have normally added by:
@@ -127,6 +127,7 @@ def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> N
"-h", "--help", action="store_true", help="show this help message"
)
_ = other_args
+ return None, None
def __get_command_func(controller, command: str):
@@ -216,7 +217,7 @@ def __patch_controller_functions(controller):
target=controller,
attribute="parse_simple_args",
side_effect=__mock_parse_simple_args,
- return_value=None,
+ return_value=(None, None),
),
patch.object(
target=controller,
diff --git a/cli/openbb_cli/controllers/cli_controller.py b/cli/openbb_cli/controllers/cli_controller.py
index 004239bf436..9bacd479505 100644
--- a/cli/openbb_cli/controllers/cli_controller.py
+++ b/cli/openbb_cli/controllers/cli_controller.py
@@ -205,10 +205,11 @@ class CLIController(BaseController):
choices["results"] = {
"--help": None,
"-h": "--help",
- "--export": None,
+ "--export": {c: None for c in ["csv", "json", "xlsx", "png", "jpg"]},
"--index": None,
"--key": None,
"--chart": None,
+ "--sheet_name": None,
}
self.update_completer(choices)
diff --git a/cli/openbb_cli/controllers/settings_controller.py b/cli/openbb_cli/controllers/settings_controller.py
index da8add50b52..ab1099362f0 100644
--- a/cli/openbb_cli/controllers/settings_controller.py
+++ b/