diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-06-26 07:51:12 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-26 07:51:12 -0700 |
commit | a0538d398c7704f64b338d59d08e999f961851a4 (patch) | |
tree | 8b623855970e8dff13611bbde2e530d41c2b5be8 | |
parent | f532097276cf57589953fbfe935caecb286d8e50 (diff) | |
parent | 34122c4524df379d1b17ccd9c1bbae6455e046ba (diff) |
Merge branch 'develop' into feature/delay-import
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/ |