diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b617161d3b1..bd40316aaa5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: key: ${{ runner.os }}-v1-linting restore-keys: ${{ runner.os }}-v1-linting - - run: python custom_pre_commit/check_doc.py --ignore-files=settings_controller.py,keys_controller.py,featflags_controller.py,degiro_controller.py,oanda_controller.py --ignore-commands=login,al_swe,info_swe,goodness,resources,yf,yolo + - run: python custom_pre_commit/check_doc.py --ignore-files=settings_controller.py,keys_controller.py,featflags_controller.py,degiro_controller.py,oanda_controller.py --ignore-commands=login,al_swe,info_swe,goodness,resources,yf,yolo,featflags,guess,settings,survey - run: pip install bandit black codespell flake8 mypy pyupgrade safety pylint==2.15.2 - run: pip install types-pytz types-requests types-termcolor types-tabulate types-PyYAML types-python-dateutil types-setuptools types-six - run: bandit -x ./tests -r . || true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d74df8cb67..886ae36b208 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: args: [ "--ignore-files=settings_controller.py,featflags_controller.py,keys_controller.py,degiro_controller.py,", - "--ignore-commands=login,al_swe,info_swe,goodness,resources,yf,yolo,from,to", + "--ignore-commands=login,al_swe,info_swe,goodness,resources,yf,yolo,from,to,featflags,guess,settings,survey", ] entry: python custom_pre_commit/check_doc.py - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/openbb_terminal/core/config/paths.py b/openbb_terminal/core/config/paths.py index 622b7271123..2dc818a0628 100644 --- a/openbb_terminal/core/config/paths.py +++ b/openbb_terminal/core/config/paths.py @@ -18,16 +18,16 @@ def get_user_data_directory(): HOME_DIRECTORY = Path.home() REPOSITORY_DIRECTORY = Path(__file__).parent.parent.parent.parent -SETTINGS_DIRECTORY = HOME_DIRECTORY / ".openbb_terminal" +PACKAGE_DIRECTORY = Path(__file__).parent.parent.parent +MISCELLANEOUS_DIRECTORY = PACKAGE_DIRECTORY / "miscellaneous" REPOSITORY_ENV_FILE = REPOSITORY_DIRECTORY / ".env" + +SETTINGS_DIRECTORY = HOME_DIRECTORY / ".openbb_terminal" USER_ENV_FILE = SETTINGS_DIRECTORY / ".env" - USER_DATA_DIRECTORY = get_user_data_directory() - USER_EXPORTS_DIRECTORY = USER_DATA_DIRECTORY / "exports" - -CUSTOM_IMPORTS_DIRECTORY = USER_DATA_DIRECTORY / "custom_imports" -PORTFOLIO_DATA_DIRECTORY = USER_DATA_DIRECTORY / "portfolio" -ROUTINES_DIRECTORY = USER_DATA_DIRECTORY / "routines" -DATA_SOURCES_DEFAULT_FILE = USER_DATA_DIRECTORY / "data_sources_default.json" -PRESETS_DIRECTORY = USER_DATA_DIRECTORY / "presets" +USER_CUSTOM_IMPORTS_DIRECTORY = USER_DATA_DIRECTORY / "custom_imports" +USER_PORTFOLIO_DATA_DIRECTORY = USER_DATA_DIRECTORY / "portfolio" +USER_ROUTINES_DIRECTORY = USER_DATA_DIRECTORY / "routines" +USER_DATA_SOURCES_DEFAULT_FILE = USER_DATA_DIRECTORY / "data_sources_default.json" +USER_PRESETS_DIRECTORY = USER_DATA_DIRECTORY / "presets" diff --git a/openbb_terminal/core/config/paths_helper.py b/openbb_terminal/core/config/paths_helper.py index 0625238996d..d5a2ec1f9cd 100644 --- a/openbb_terminal/core/config/paths_helper.py +++ b/openbb_terminal/core/config/paths_helper.py @@ -6,12 +6,12 @@ from openbb_terminal.core.config.paths import ( USER_DATA_DIRECTORY, USER_ENV_FILE, REPOSITORY_ENV_FILE, - CUSTOM_IMPORTS_DIRECTORY, + USER_CUSTOM_IMPORTS_DIRECTORY, USER_EXPORTS_DIRECTORY, - PORTFOLIO_DATA_DIRECTORY, - PRESETS_DIRECTORY, - ROUTINES_DIRECTORY, - DATA_SOURCES_DEFAULT_FILE, + USER_PORTFOLIO_DATA_DIRECTORY, + USER_PRESETS_DIRECTORY, + USER_ROUTINES_DIRECTORY, + USER_DATA_SOURCES_DEFAULT_FILE, ) # pylint: disable=W0603 @@ -43,25 +43,25 @@ dirs_list = [ SETTINGS_DIRECTORY, USER_DATA_DIRECTORY, USER_DATA_DIRECTORY / "styles", - CUSTOM_IMPORTS_DIRECTORY, - CUSTOM_IMPORTS_DIRECTORY / "econometrics", - CUSTOM_IMPORTS_DIRECTORY / "stocks", - CUSTOM_IMPORTS_DIRECTORY / "dashboards", + USER_CUSTOM_IMPORTS_DIRECTORY, + USER_CUSTOM_IMPORTS_DIRECTORY / "econometrics", + USER_CUSTOM_IMPORTS_DIRECTORY / "stocks", + USER_CUSTOM_IMPORTS_DIRECTORY / "dashboards", USER_EXPORTS_DIRECTORY, USER_EXPORTS_DIRECTORY / "reports", - PORTFOLIO_DATA_DIRECTORY, - PORTFOLIO_DATA_DIRECTORY / "views", - PORTFOLIO_DATA_DIRECTORY / "holdings", - PORTFOLIO_DATA_DIRECTORY / "allocation", - PORTFOLIO_DATA_DIRECTORY / "optimization", - PRESETS_DIRECTORY, - PRESETS_DIRECTORY / "stocks" / "options", - PRESETS_DIRECTORY / "stocks" / "screener", - PRESETS_DIRECTORY / "stocks" / "insider", - PRESETS_DIRECTORY / "etf" / "screener", - ROUTINES_DIRECTORY, + USER_PORTFOLIO_DATA_DIRECTORY, + USER_PORTFOLIO_DATA_DIRECTORY / "views", + USER_PORTFOLIO_DATA_DIRECTORY / "holdings", + USER_PORTFOLIO_DATA_DIRECTORY / "allocation", + USER_PORTFOLIO_DATA_DIRECTORY / "optimization", + USER_PRESETS_DIRECTORY, + USER_PRESETS_DIRECTORY / "stocks" / "options", + USER_PRESETS_DIRECTORY / "stocks" / "screener", + USER_PRESETS_DIRECTORY / "stocks" / "insider", + USER_PRESETS_DIRECTORY / "etf" / "screener", + USER_ROUTINES_DIRECTORY, ] -dirs_files = [USER_ENV_FILE, REPOSITORY_ENV_FILE, DATA_SOURCES_DEFAULT_FILE] +dirs_files = [USER_ENV_FILE, REPOSITORY_ENV_FILE, USER_DATA_SOURCES_DEFAULT_FILE] initialized = False diff --git a/openbb_terminal/econometrics/econometrics_controller.py b/openbb_terminal/econometrics/econometrics_controller.py index 52a0219c459..701ad94dfb3 100644 --- a/openbb_terminal/econometrics/econometrics_controller.py +++ b/openbb_terminal/econometrics/econometrics_controller.py @@ -17,7 +17,7 @@ from openbb_terminal.custom_prompt_toolkit import NestedCompleter import openbb_terminal.econometrics.regression_model import openbb_terminal.econometrics.regression_view -from openbb_terminal.core.config.paths import CUSTOM_IMPORTS_DIRECTORY +from openbb_terminal.core.config.paths import USER_CUSTOM_IMPORTS_DIRECTORY from openbb_terminal import feature_flags as obbff from openbb_terminal.core.config.paths import USER_EXPORTS_DIRECTORY from openbb_terminal.decorators import log_start_end @@ -165,7 +165,9 @@ class EconometricsController(BaseController): for file_type in self.file_types for filepath in chain( Path(USER_EXPORTS_DIRECTORY).rglob(f"*.{file_type}"), - Path(CUSTOM_IMPORTS_DIRECTORY / "econometrics").rglob(f"*.{file_type}"), + Path(USER_CUSTOM_IMPORTS_DIRECTORY / "econometrics").rglob( + f"*.{file_type}" + ), ) if filepath.is_file() } @@ -248,7 +250,7 @@ class EconometricsController(BaseController): mt = MenuText("econometrics/") mt.add_param( "_data_loc", - f"\n\t{str(USER_EXPORTS_DIRECTORY)}\n\t{str(CUSTOM_IMPORTS_DIRECTORY/'econometrics')}", + f"\n\t{str(USER_EXPORTS_DIRECTORY)}\n\t{str(USER_CUSTOM_IMPORTS_DIRECTORY/'econometrics')}", ) mt.add_raw("\n") mt.add_cmd("load") diff --git a/openbb_terminal/etf/screener/screener_model.py b/openbb_terminal/etf/screener/screener_model.py index 936ec444729..87c9775cd26 100644 --- a/openbb_terminal/etf/screener/screener_model.py +++ b/openbb_terminal/etf/screener/screener_model.py @@ -9,7 +9,7 @@ from typing import Dict import pandas as pd from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import PRESETS_DIRECTORY +from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY logger = logging.getLogger(__name__) @@ -21,7 +21,7 @@ def get_preset_choices() -> Dict: filepath as value """ - PRESETS_PATH = PRESETS_DIRECTORY / "etf" / "screener" + PRESETS_PATH = USER_PRESETS_DIRECTORY / "etf" / "screener" PRESETS_PATH_DEFAULT = Path(__file__).parent / "presets" preset_choices = { filepath.name: filepath diff --git a/openbb_terminal/feature_flags.py b/openbb_terminal/feature_flags.py index 5887a21e46f..fbb8bc35657 100644 --- a/openbb_terminal/feature_flags.py +++ b/openbb_terminal/feature_flags.py @@ -12,7 +12,7 @@ import i18n from openbb_terminal.core.config.paths import ( USER_ENV_FILE, REPOSITORY_ENV_FILE, - DATA_SOURCES_DEFAULT_FILE, + USER_DATA_SOURCES_DEFAULT_FILE, ) from openbb_terminal.core.config import paths_helper @@ -110,7 +110,7 @@ LOGGING_COMMIT_HASH = str(os.getenv("OPENBB_LOGGING_COMMIT_HASH", "REPLACE_ME")) PREFERRED_DATA_SOURCE_FILE = str( os.getenv( "OPENBB_PREFERRED_DATA_SOURCE_FILE", - DATA_SOURCES_DEFAULT_FILE, + USER_DATA_SOURCES_DEFAULT_FILE, ) ) diff --git a/openbb_terminal/forecast/forecast_controller.py b/openbb_terminal/forecast/forecast_controller.py index 58b29ca7e33..819dcee907c 100644 --- a/openbb_terminal/forecast/forecast_controller.py +++ b/openbb_terminal/forecast/forecast_controller.py @@ -55,7 +55,7 @@ from openbb_terminal.forecast import ( ) from openbb_terminal.core.config.paths import ( USER_EXPORTS_DIRECTORY, - CUSTOM_IMPORTS_DIRECTORY, + USER_CUSTOM_IMPORTS_DIRECTORY, ) logger = logging.getLogger(__name__) @@ -177,7 +177,7 @@ class ForecastController(BaseController): for file_type in self.file_types for filepath in chain( USER_EXPORTS_DIRECTORY.rglob(f"*.{file_type}"), - CUSTOM_IMPORTS_DIRECTORY.rglob(f"*.{file_type}"), + USER_CUSTOM_IMPORTS_DIRECTORY.rglob(f"*.{file_type}"), ) if filepath.is_file() } @@ -287,7 +287,8 @@ class ForecastController(BaseController): mt.add_param("_disclaimer_", self.disclaimer) mt.add_raw("\n") mt.add_param( - "_data_loc", f"\n\t{USER_EXPORTS_DIRECTORY}\n\t{CUSTOM_IMPORTS_DIRECTORY}" + "_data_loc", + f"\n\t{USER_EXPORTS_DIRECTORY}\n\t{USER_CUSTOM_IMPORTS_DIRECTORY}", ) mt.add_raw("\n") mt.add_cmd("load") diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 94db17eb5c5..8095925eb7d 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -21,8 +21,8 @@ import pandas as pd import numpy as np from openbb_terminal.core.config.paths import ( - CUSTOM_IMPORTS_DIRECTORY, - ROUTINES_DIRECTORY, + USER_CUSTOM_IMPORTS_DIRECTORY, + USER_ROUTINES_DIRECTORY, ) from openbb_terminal.decorators import log_start_end @@ -605,11 +605,11 @@ class BaseController(metaclass=ABCMeta): "[red]There is no session to be saved. Run at least 1 command after starting 'record'[/red]\n" ) else: - routine_file = os.path.join(ROUTINES_DIRECTORY, SESSION_RECORDED_NAME) + routine_file = os.path.join(USER_ROUTINES_DIRECTORY, SESSION_RECORDED_NAME) if os.path.isfile(routine_file): routine_file = os.path.join( - ROUTINES_DIRECTORY, + USER_ROUTINES_DIRECTORY, datetime.now().strftime("%Y%m%d_%H%M%S_") + SESSION_RECORDED_NAME, ) @@ -1005,7 +1005,7 @@ class StockBaseController(BaseController, metaclass=ABCMeta): # This seems to block the .exe since the folder needs to be manually created # This block makes sure that we only look for the file if the -f flag is used # Adding files in the argparse choices, will fail for the .exe even without -f - STOCKS_CUSTOM_IMPORTS = CUSTOM_IMPORTS_DIRECTORY / "stocks" + STOCKS_CUSTOM_IMPORTS = USER_CUSTOM_IMPORTS_DIRECTORY / "stocks" try: file_list = [x.name for x in STOCKS_CUSTOM_IMPORTS.iterdir()] if ns_parser.filepath not in file_list: diff --git a/openbb_terminal/portfolio/portfolio_analysis/pa_controller.py b/openbb_terminal/portfolio/portfolio_analysis/pa_controller.py index 66d25d7cd0f..8cd4a0f3f4d 100644 --- a/openbb_terminal/portfolio/portfolio_analysis/pa_controller.py +++ b/openbb_terminal/portfolio/portfolio_analysis/pa_controller.py @@ -9,7 +9,7 @@ from typing import List import pandas as pd from openbb_terminal import feature_flags as obbff -from openbb_terminal.core.config.paths import PORTFOLIO_DATA_DIRECTORY +from openbb_terminal.core.config.paths import USER_PORTFOLIO_DATA_DIRECTORY from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.menu import session @@ -22,7 +22,7 @@ from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) -portfolios_path = PORTFOLIO_DATA_DIRECTORY / "portfolios" +portfolios_path = USER_PORTFOLIO_DATA_DIRECTORY / "portfolios" port_types = [".csv", ".json", ".xlsx"] possible_paths = { portpath.name: portpath diff --git a/openbb_terminal/portfolio/portfolio_helper.py b/openbb_terminal/portfolio/portfolio_helper.py index f66410d0148..23cc45ee85b 100644 --- a/openbb_terminal/portfolio/portfolio_helper.py +++ b/openbb_terminal/portfolio/portfolio_helper.py @@ -14,7 +14,7 @@ import pandas as pd import numpy as np from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import PORTFOLIO_DATA_DIRECTORY +from openbb_terminal.core.config.paths import USER_PORTFOLIO_DATA_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal.portfolio.statics import REGIONS, PERIODS @@ -46,7 +46,7 @@ PERIODS_DAYS = { "10y": 10 * 12 * 21, } -DEFAULT_HOLDINGS_PATH = PORTFOLIO_DATA_DIRECTORY / "holdings" +DEFAULT_HOLDINGS_PATH = USER_PORTFOLIO_DATA_DIRECTORY / "holdings" def is_ticker(ticker: str) -> bool: diff --git a/openbb_terminal/portfolio/portfolio_optimization/po_controller.py b/openbb_terminal/portfolio/portfolio_optimization/po_controller.py index 80f0e4698c1..e3e3d276133 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/po_controller.py +++ b/openbb_terminal/portfolio/portfolio_optimization/po_controller.py @@ -13,7 +13,7 @@ from openbb_terminal import feature_flags as obbff from openbb_terminal import parent_classes from openbb_terminal.core.config.paths import ( USER_EXPORTS_DIRECTORY, - PORTFOLIO_DATA_DIRECTORY, + USER_PORTFOLIO_DATA_DIRECTORY, REPOSITORY_DIRECTORY, ) from openbb_terminal.decorators import log_start_end @@ -329,7 +329,7 @@ class PortfolioOptimizationController(BaseController): "property", ] self.file_types = ["xlsx", "ini"] - self.DEFAULT_ALLOCATION_PATH = PORTFOLIO_DATA_DIRECTORY / "allocation" + self.DEFAULT_ALLOCATION_PATH = USER_PORTFOLIO_DATA_DIRECTORY / "allocation" self.DATA_ALLOCATION_FILES = { filepath.name: filepath @@ -348,7 +348,7 @@ class PortfolioOptimizationController(BaseController): ) self.current_file = "" - self.DEFAULT_OPTIMIZATION_PATH = PORTFOLIO_DATA_DIRECTORY / "optimization" + self.DEFAULT_OPTIMIZATION_PATH = USER_PORTFOLIO_DATA_DIRECTORY / "optimization" self.DATA_OPTIMIZATION_FILES = { filepath.name: filepath @@ -2484,7 +2484,7 @@ class PortfolioOptimizationController(BaseController): return if ns_parser.file: - excel_file = PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file + excel_file = USER_PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file p_views, q_views = excel_model.load_bl_views(excel_file=excel_file) else: p_views = ns_parser.p_views @@ -2531,7 +2531,9 @@ class PortfolioOptimizationController(BaseController): if table is False: if ns_parser.file_sa: - excel_file = PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file_sa + excel_file = ( + USER_PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file_sa + ) p_views_sa, q_views_sa = excel_model.load_bl_views( excel_file=excel_file ) diff --git a/openbb_terminal/stocks/insider/openinsider_model.py b/openbb_terminal/stocks/insider/openinsider_model.py index f2b6a7bf246..60a7aaa12f2 100644 --- a/openbb_terminal/stocks/insider/openinsider_model.py +++ b/openbb_terminal/stocks/insider/openinsider_model.py @@ -10,7 +10,7 @@ import requests from bs4 import BeautifulSoup from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import PRESETS_DIRECTORY +from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) @@ -1109,7 +1109,7 @@ def get_preset_choices() -> Dict: filepath as value """ - PRESETS_PATH = PRESETS_DIRECTORY / "stocks" / "insider" + PRESETS_PATH = USER_PRESETS_DIRECTORY / "stocks" / "insider" PRESETS_PATH_DEFAULT = Path(__file__).parent / "presets" preset_choices = { filepath.name: filepath diff --git a/openbb_terminal/stocks/options/screen/syncretism_model.py b/openbb_terminal/stocks/options/screen/syncretism_model.py index 55b54e92a72..69e3c5ed04a 100644 --- a/openbb_terminal/stocks/options/screen/syncretism_model.py +++ b/openbb_terminal/stocks/options/screen/syncretism_model.py @@ -11,7 +11,7 @@ import requests import yfinance as yf from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import PRESETS_DIRECTORY +from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal.stocks.options import yfinance_model @@ -120,7 +120,7 @@ def get_preset_choices() -> Dict: filepath as value """ - PRESETS_PATH = PRESETS_DIRECTORY / "stocks" / "options" + PRESETS_PATH = USER_PRESETS_DIRECTORY / "stocks" / "options" PRESETS_PATH_DEFAULT = Path(__file__).parent.parent / "presets" preset_choices = { filepath.name: filepath diff --git a/openbb_terminal/stocks/screener/finviz_model.py b/openbb_terminal/stocks/screener/finviz_model.py index b1f80d61570..c7eb2f85386 100644 --- a/openbb_terminal/stocks/screener/finviz_model.py +++ b/openbb_terminal/stocks/screener/finviz_model.py @@ -13,12 +13,12 @@ from finvizfinance.screener import ( ) from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import PRESETS_DIRECTORY +from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) -PRESETS_PATH = PRESETS_DIRECTORY / "stocks" / "screener" +PRESETS_PATH = USER_PRESETS_DIRECTORY / "stocks" / "screener" PRESETS_PATH_DEFAULT = Path(__file__).parent / "presets" preset_choices = { filepath.name: filepath diff --git a/openbb_terminal/stocks/screener/screener_controller.py b/openbb_terminal/stocks/screener/screener_controller.py index 40637d8e7f1..8b62a8a92d6 100644 --- a/openbb_terminal/stocks/screener/screener_controller.py +++ b/openbb_terminal/stocks/screener/screener_controller.py @@ -10,7 +10,7 @@ from typing import List from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal import feature_flags as obbff -from openbb_terminal.core.config.paths import PRESETS_DIRECTORY +from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_classes import AllowArgsWithWhiteSpace from openbb_terminal.helper_funcs import ( @@ -37,7 +37,7 @@ logger = logging.getLogger(__name__) # TODO: HELP WANTED! This menu required some refactoring. Things that can be addressed: # - better preset management (MVC style). -PRESETS_PATH = PRESETS_DIRECTORY / "stocks" / "screener" +PRESETS_PATH = USER_PRESETS_DIRECTORY / "stocks" / "screener" class ScreenerController(BaseController): diff --git a/openbb_terminal/stocks/screener/screener_view.py b/openbb_terminal/stocks/screener/screener_view.py index 1fab02e9fd9..5619a45a23c 100644 --- a/openbb_terminal/stocks/screener/screener_view.py +++ b/openbb_terminal/stocks/screener/screener_view.py @@ -5,10 +5,10 @@ import configparser from pathlib import Path from openbb_terminal.rich_config import console -from openbb_terminal.core.config.paths import PRESETS_DIRECTORY +from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY from openbb_terminal.stocks.screener import finviz_model -PRESETS_PATH = PRESETS_DIRECTORY / "stocks" / "screener" +PRESETS_PATH = USER_PRESETS_DIRECTORY / "stocks" / "screener" PRESETS_PATH_DEFAULT = Path(__file__).parent / "presets" preset_choices = { filepath.name: filepath diff --git a/openbb_terminal/stocks/screener/yahoofinance_model.py b/openbb_terminal/stocks/screener/yahoofinance_model.py index f8b0aa0e04e..5d2510b8586 100644 --- a/openbb_terminal/stocks/screener/yahoofinance_model.py +++ b/openbb_terminal/stocks/screener/yahoofinance_model.py @@ -16,7 +16,7 @@ from sklearn.preprocessing import MinMaxScaler from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import PRESETS_DIRECTORY +from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal.stocks.screener import finviz_model @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) register_matplotlib_converters() -PRESETS_PATH = PRESETS_DIRECTORY / "stocks" / "screener" +PRESETS_PATH = USER_PRESETS_DIRECTORY / "stocks" / "screener" PRESETS_PATH_DEFAULT = Path(__file__).parent / "presets" preset_choices = { filepath.name: filepath diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py new file mode 100644 index 00000000000..ce8847b1d42 --- /dev/null +++ b/openbb_terminal/terminal_controller.py @@ -0,0 +1,1030 @@ +#!/usr/bin/env python +"""Main Terminal Module""" +__docformat__ = "numpy" + +import argparse +import difflib +import logging +import os +import platform +import sys +import webbrowser +from typing import List +import dotenv + +from prompt_toolkit import PromptSession +from prompt_toolkit.completion import NestedCompleter +from prompt_toolkit.styles import Style +from prompt_toolkit.formatted_text import HTML +import pandas as pd + +from openbb_terminal import feature_flags as obbff + +from openbb_terminal.core.config.paths import ( + REPOSITORY_DIRECTORY, + USER_ENV_FILE, + REPOSITORY_ENV_FILE, + HOME_DIRECTORY, + USER_ROUTINES_DIRECTORY, +) +from openbb_terminal.core.log.generation.path_tracking_file_handler import ( + PathTrackingFileHandler, +) + +from openbb_terminal.helper_funcs import ( + check_positive, + get_flair, + parse_simple_args, + EXPORT_ONLY_RAW_DATA_ALLOWED, +) +from openbb_terminal.loggers import setup_logging +from openbb_terminal.menu import session +from openbb_terminal.parent_classes import BaseController +from openbb_terminal.rich_config import console, MenuText, translate +from openbb_terminal.terminal_helper import ( + bootup, + check_for_updates, + is_reset, + print_goodbye, + reset, + suppress_stdout, + update_terminal, + welcome_message, +) +from openbb_terminal.helper_funcs import parse_and_split_input +from openbb_terminal.common import feedparser_view + +# pylint: disable=too-many-public-methods,import-outside-toplevel,too-many-branches,no-member,C0302 + +logger = logging.getLogger(__name__) + +env_file = str(USER_ENV_FILE) + + +class TerminalController(BaseController): + """Terminal Controller class""" + + CHOICES_COMMANDS = [ + "keys", + "settings", + "survey", + "update", + "featflags", + "exe", + "guess", + "news", + ] + CHOICES_MENUS = [ + "stocks", + "economy", + "crypto", + "portfolio", + "forex", + "etf", + "reports", + "dashboards", + "funds", + "alternative", + "econometrics", + "sources", + "forecast", + ] + + PATH = "/" + + GUESS_TOTAL_TRIES = 0 + GUESS_NUMBER_TRIES_LEFT = 0 + GUESS_SUM_SCORE = 0.0 + GUESS_CORRECTLY = 0 + + def __init__(self, jobs_cmds: List[str] = None): + """Constructor""" + super().__init__(jobs_cmds) + + self.queue: List[str] = list() + + if jobs_cmds: + self.queue = parse_and_split_input( + an_input=" ".join(jobs_cmds), custom_filters=[] + ) + + self.update_success = False + + self.update_runtime_choices() + + def update_runtime_choices(self): + """Update runtime choices""" + self.ROUTINE_FILES = { + filepath.name: filepath + for filepath in (REPOSITORY_DIRECTORY / "routines").rglob("*.openbb") + } + self.ROUTINE_FILES.update( + { + filepath.name: filepath + for filepath in USER_ROUTINES_DIRECTORY.rglob("*.openbb") + } + ) + self.ROUTINE_CHOICES = {filename: None for filename in self.ROUTINE_FILES} + if session and obbff.USE_PROMPT_TOOLKIT: + choices: dict = {c: {} for c in self.controller_choices} + choices["support"] = self.SUPPORT_CHOICES + choices["exe"] = self.ROUTINE_CHOICES + + self.completer = NestedCompleter.from_nested_dict(choices) + + def print_help(self): + """Print help""" + mt = MenuText("") + mt.add_info("_home_") + mt.add_cmd("about") + mt.add_cmd("support") + mt.add_cmd("survey") + mt.add_cmd("update") + mt.add_cmd("wiki") + mt.add_cmd("record") + mt.add_cmd("stop") + mt.add_raw("\n") + mt.add_info("_configure_") + mt.add_menu("keys") + mt.add_menu("featflags") + mt.add_menu("sources") + mt.add_menu("settings") + mt.add_raw("\n") + mt.add_cmd("news") + mt.add_cmd("exe") + mt.add_raw("\n") + mt.add_info("_main_menu_") + mt.add_menu("stocks") + mt.add_menu("crypto") + mt.add_menu("etf") + mt.add_menu("economy") + mt.add_menu("forex") + mt.add_menu("funds") + mt.add_menu("alternative") + mt.add_raw("\n") + mt.add_info("_others_") + mt.add_menu("econometrics") + mt.add_menu("forecast") + mt.add_menu("portfolio") + mt.add_menu("dashboards") + mt.add_menu("reports") + mt.add_raw("\n") + console.print(text=mt.menu_text, menu="Home") + self.update_runtime_choices() + + def call_news(self, other_args: List[str]) -> None: + """Process news command""" + parse = argparse.ArgumentParser( + add_help=False, + prog="news", + description=translate("news"), + ) + parse.add_argument( + "-t", + "--term", + dest="term", + default=[""], + nargs="+", + help="search for a term on the news", + ) + parse.add_argument( + "-s", + "--sources", + dest="sources", + default=["bloomberg.com"], + nargs="+", + help="sources from where to get news from", + ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-t") + news_parser = self.parse_known_args_and_warn( + parse, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED, limit=5 + ) + if news_parser: + feedparser_view.display_news( + term=" ".join(news_parser.term), + sources=" ".join(news_parser.sources), + limit=news_parser.limit, + export=news_parser.export, + ) + + def call_guess(self, other_args: List[str]) -> None: + """Process guess command""" + import time + import json + import random + + if self.GUESS_NUMBER_TRIES_LEFT == 0 and self.GUESS_SUM_SCORE < 0.01: + parser_exe = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="guess", + description="Guess command to achieve task successfully.", + ) + parser_exe.add_argument( + "-l", + "--limit", + type=check_positive, + help="Number of tasks to attempt.", + dest="limit", + default=1, + ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-l") + ns_parser_guess = parse_simple_args(parser_exe, other_args) + + if self.GUESS_TOTAL_TRIES == 0: + self.GUESS_NUMBER_TRIES_LEFT = ns_parser_guess.limit + self.GUESS_SUM_SCORE = 0 + self.GUESS_TOTAL_TRIES = ns_parser_guess.limit + + try: + with open(obbff.GUESS_EASTER_EGG_FILE) as f: + # Load the file as a JSON document + json_doc = json.load(f) + + task = random.choice(list(json_doc.keys())) # nosec + solution = json_doc[task] + + start = time.time() + console.print(f"\n[yellow]{task}[/yellow]\n") + if isinstance(session, PromptSession): + an_input = session.prompt("GUESS / $ ") + else: + an_input = "" + time_dif = time.time() - start + + # When there are multiple paths to same solution + if isinstance(solution, List): + if an_input.lower() in [s.lower() for s in solution]: + self.queue = an_input.split("/") + ["home"] + console.print( + f"\n[green]You guessed correctly in {round(time_dif, 2)} seconds![green]\n" + ) + # If we are already counting successes + if self.GUESS_TOTAL_TRIES > 0: + self.GUESS_CORRECTLY += 1 + self.GUESS_SUM_SCORE += time_dif + else: + solutions_texts = "\n".join(solution) + console.print( + f"\n[red]You guessed wrong! The correct paths would have been:\n{solutions_texts}[/red]\n" + ) + + # When there is a single path to the solution + else: + if an_input.lower() == solution.lower(): + self.queue = an_input.split("/") + ["home"] + console.print( + f"\n[green]You guessed correctly in {round(time_dif, 2)} seconds![green]\n" + ) + # If we are already counting successes + if self.GUESS_TOTAL_TRIES > 0: + self.GUESS_CORRECTLY += 1 + self.GUESS_SUM_SCORE += time_dif + else: + console.print( + f"\n[red]You guessed wrong! The correct path would have been:\n{solution}[/red]\n" + ) + + # Compute average score and provide a result if it's the last try + if self.GUESS_TOTAL_TRIES > 0: + + self.GUESS_NUMBER_TRIES_LEFT -= 1 + if self.GUESS_NUMBER_TRIES_LEFT == 0 and self.GUESS_TOTAL_TRIES > 1: + color = ( + "green" + if self.GUESS_CORRECTLY == self.GUESS_TOTAL_TRIES + else "red" + ) + console.print( + f"[{color}]OUTCOME: You got {int(self.GUESS_CORRECTLY)} out of" + f" {int(self.GUESS_TOTAL_TRIES)}.[/{color}]\n" + ) + if self.GUESS_CORRECTLY == self.GUESS_TOTAL_TRIES: + avg = self.GUESS_SUM_SCORE / self.GUESS_TOTAL_TRIES + console.print( + f"[green]Average score: {round(avg, 2)} seconds![/green]\n" + ) + self.GUESS_TOTAL_TRIES = 0 + self.GUESS_CORRECTLY = 0 + self.GUESS_SUM_SCORE = 0 + else: + self.queue += ["guess"] + + except Exception as e: + console.print( + f"[red]Failed to load guess game from file: " + f"{obbff.GUESS_EASTER_EGG_FILE}[/red]" + ) + console.print(f"[red]{e}[/red]") + + @staticmethod + def call_survey(_) -> None: + """Process survey command""" + webbrowser.open("https://openbb.co/survey") + + def call_update(self, _): + """Process update command""" + if not obbff.PACKAGED_APPLICATION: + self.update_success = not update_terminal() + else: + console.print( + "Find the most recent release of the OpenBB Terminal here: " + "https://openbb.co/products/terminal#get-started\n" + ) + + def call_keys(self, _): + """Process keys command""" + from openbb_terminal.keys_controller import KeysController + + self.queue = self.load_class(KeysController, self.queue, env_file) + + def call_settings(self, _): + """Process settings command""" + from openbb_terminal.settings_controller import SettingsController + + self.queue = self.load_class(SettingsController, self.queue, env_file) + + def call_featflags(self, _): + """Process feature flags command""" + from openbb_terminal.featflags_controller import FeatureFlagsController + + self.queue = self.load_class(FeatureFlagsController, self.queue) + + def call_stocks(self, _): + """Process stocks command""" + from openbb_terminal.stocks.stocks_controller import StocksController + + self.queue = self.load_class(StocksController, self.queue) + + def call_crypto(self, _): + """Process crypto command""" + from openbb_terminal.cryptocurrency.crypto_controller import CryptoController + + self.queue = self.load_class(CryptoController, self.queue) + + def call_economy(self, _): + """Process economy command""" + from openbb_terminal.economy.economy_controller import EconomyController + + self.queue = self.load_class(EconomyController, self.queue) + + def call_etf(self, _): + """Process etf command""" + from openbb_terminal.etf.etf_controller import ETFController + + self.queue = self.load_class(ETFController, self.queue) + + def call_funds(self, _): + """Process etf command""" + from openbb_terminal.mutual_funds.mutual_fund_controller import ( + FundController, + ) + + self.queue = self.load_class(FundController, self.queue) + + def call_forex(self, _): + """Process forex command""" + from openbb_terminal.forex.forex_controller import ForexController + + self.queue = self.load_class(ForexController, self.queue) + + def call_reports(self, _): + """Process reports command""" + from openbb_terminal.reports.reports_controller import ( + ReportController, + ) + + self.queue = self.load_class(ReportController, self.queue) + + def call_dashboards(self, _): + """Process dashboards command""" + if not obbff.PACKAGED_APPLICATION: + from openbb_terminal.dashboards.dashboards_controller import ( + DashboardsController, + ) + + self.queue = self.load_class(DashboardsController, self.queue) + else: + console.print("This feature is coming soon.") + console.print( + "Use the source code and an Anaconda environment if you are familiar with Python." + ) + + def call_alternative(self, _): + """Process alternative command""" + from openbb_terminal.alternative.alt_controller import ( + AlternativeDataController, + ) + + self.queue = self.load_class(AlternativeDataController, self.queue) + + def call_econometrics(self, _): + """Process econometrics command""" + from openbb_terminal.econometrics.econometrics_controller import ( + EconometricsController, + ) + + self.queue = EconometricsController(self.queue).menu() + + def call_forecast(self, _): + """Process forecast command""" + from openbb_terminal.forecast.forecast_controller import ( + ForecastController, + ) + + self.queue = self.load_class(ForecastController, "", pd.DataFrame(), self.queue) + + def call_portfolio(self, _): + """Process portfolio command""" + from openbb_terminal.portfolio.portfolio_controller import ( + PortfolioController, + ) + + self.queue = self.load_class(PortfolioController, self.queue) + + def call_sources(self, _): + """Process sources command""" + from openbb_terminal.sources_controller import SourcesController + + self.queue = self.load_class(SourcesController, self.queue) + + def call_exe(self, other_args: List[str]): + """Process exe command""" + # Merge rest of string path to other_args and remove queue since it is a dir + other_args += self.queue + + if not other_args: + console.print( + "[red]Provide a path to the routine you wish to execute.\n[/red]" + ) + return + + full_input = " ".join(other_args) + if " " in full_input: + other_args_processed = full_input.split(" ") + else: + other_args_processed = [full_input] + self.queue = [] + + path_routine = "" + args = list() + for idx, path_dir in enumerate(other_args_processed): + if path_dir in ("-i", "--input"): + args = [path_routine[1:]] + other_args_processed[idx:] + break + if path_dir not in ("--file"): + path_routine += f"/{path_dir}" + + if not args: + args = [path_routine[1:]] + + parser_exe = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="exe", + description="Execute automated routine script.", + ) + parser_exe.add_argument( + "--file", + help="The path or .openbb file to run.", + dest="path", + default="", + required="-h" not in args, + ) + parser_exe.add_argument( + "-i", + "--input", + help="Select multiple inputs to be replaced in the routine and separated by commas. E.g. GME,AMC,BTC-USD", + dest="routine_args", + type=lambda s: [str(item) for item in s.split(",")], + ) + if args and "-" not in args[0][0]: + args.insert(0, "--file") + ns_parser_exe = parse_simple_args(parser_exe, args) + if ns_parser_exe: + if ns_parser_exe.path: + if ns_parser_exe.path in self.ROUTINE_CHOICES: + path = self.ROUTINE_FILES[ns_parser_exe.path] + else: + path = ns_parser_exe.path + + with open(path) as fp: + raw_lines = [ + x for x in fp if (not is_reset(x)) and ("#" not in x) and x + ] + raw_lines = [ + raw_line.strip("\n") + for raw_line in raw_lines + if raw_line.strip("\n") + ] + + lines = list() + for rawline in raw_lines: + templine = rawline + + # Check if dynamic parameter exists in script + if "$ARGV" in rawline: + # Check if user has provided inputs through -i or --input + if ns_parser_exe.routine_args: + for i, arg in enumerate(ns_parser_exe.routine_args): + # Check what is the location of the ARGV to be replaced + if f"$ARGV[{i}]" in templine: + templine = templine.replace(f"$ARGV[{i}]", arg) + + # Check if all ARGV have been removed, otherwise means that there are less inputs + # when running the script than the script expects + if "$ARGV" in templine: + console.print( + "[red]Not enough inputs were provided to fill in dynamic variables. " + "E.g. --input VAR1,VAR2,VAR3[/red]\n" + ) + return + + lines.append(templine) + # The script expects a parameter that the user has not provided + else: + console.print( + "[red]The script expects parameters, " + "run the script again with --input defined.[/red]\n" + ) + return + else: + lines.append(templine) + + simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}" + file_cmds = simulate_argv.replace("//", "/home/").split() + file_cmds = ( + insert_start_slash(file_cmds) if file_cmds else file_cmds + ) + cmds_with_params = " ".join(file_cmds) + self.queue = [ + val + for val in parse_and_split_input( + an_input=cmds_with_params, custom_filters=[] + ) + if val + ] + + if "export" in self.queue[0]: + export_path = self.queue[0].split(" ")[1] + # If the path selected does not start from the user root, give relative location from root + if export_path[0] == "~": + export_path = export_path.replace( + "~", HOME_DIRECTORY.as_posix() + ) + elif export_path[0] != "/": + export_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), export_path + ) + + # Check if the directory exists + if os.path.isdir(export_path): + console.print( + f"Export data to be saved in the selected folder: '{export_path}'" + ) + else: + os.makedirs(export_path) + console.print( + f"[green]Folder '{export_path}' successfully created.[/green]" + ) + obbff.EXPORT_FOLDER_PATH = export_path + self.queue = self.queue[1:] + + +# pylint: disable=global-statement +def terminal(jobs_cmds: List[str] = None, appName: str = "gst"): + """Terminal Menu""" + # TODO: HELP WANTED! Refactor the appName setting if a more elegant solution comes up + if obbff.PACKAGED_APPLICATION: + appName = "gst_packaged" + + setup_logging(appName) + logger.info("START") + log_settings() + + if jobs_cmds is not None and jobs_cmds: + logger.info("INPUT: %s", "/".join(jobs_cmds)) + + export_path = "" + if jobs_cmds and "export" in jobs_cmds[0]: + export_path = jobs_cmds[0].split("/")[0].split(" ")[1] + jobs_cmds = ["/".join(jobs_cmds[0].split("/")[1:])] + + ret_code = 1 + t_controller = TerminalController(jobs_cmds) + an_input = "" + + if export_path: + # If the path selected does not start from the user root, give relative location from terminal root + if export_path[0] == "~": + export_path = export_path.replace("~", HOME_DIRECTORY.as_posix()) + elif export_path[0] != "/": + export_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), export_path + ) + + # Check if the directory exists + if os.path.isdir(export_path): + console.print( + f"Export data to be saved in the selected folder: '{export_path}'" + ) + else: + os.makedirs(export_path) + console.print( + f"[green]Folder '{export_path}' successfully created.[/green]" + ) + obbff.EXPORT_FOLDER_PATH = export_path + + bootup() + if not jobs_cmds: + welcome_message() + t_controller.print_help() + check_for_updates() + + dotenv.load_dotenv(USER_ENV_FILE) + dotenv.load_dotenv(REPOSITORY_ENV_FILE, override=True) + + while ret_code: + if obbff.ENABLE_QUICK_EXIT: + console.print("Quick exit enabled") + break + + # There is a command in the queue + if t_controller.queue and len(t_controller.queue) > 0: + # If the command is quitting the menu we want to return in here + if t_controller.queue[0] in ("q", "..", "quit"): + print_goodbye() + break + + # Consume 1 element from the queue + an_input = t_controller.queue[0] + t_controller.queue = t_controller.queue[1:] + + # Print the current location because this was an instruction and we want user to know what was the action + if an_input and an_input.split(" ")[0] in t_controller.CHOICES_COMMANDS: + console.print(f"{get_flair()} / $ {an_input}") + + # Get input command from user + else: + # Get input from user using auto-completion + if session and obbff.USE_PROMPT_TOOLKIT: + try: + if obbff.TOOLBAR_HINT: + an_input = session.prompt( + f"{get_flair()} / $ ", + completer=t_controller.completer, + search_ignore_case=True, + bottom_toolbar=HTML( + ' help menu ' + ' return to previous menu ' + ' exit terminal ' + ' ' + "see usage and available options " + ' Getting Started Documentation' + ), + style=Style.from_dict( + { + "bottom-toolbar": "#ffffff bg:#333333", + } + ), + ) + else: + an_input = session.prompt( + f"{get_flair()} / $ ", + completer=t_controller.completer, + search_ignore_case=True, + ) + except (KeyboardInterrupt, EOFError): + print_goodbye() + break + # Get input from user without auto-completion + else: + an_input = input(f"{get_flair()} / $ ") + + try: + # Process the input command + t_controller.queue = t_controller.switch(an_input) + if an_input in ("q", "quit", "..", "exit"): + print_goodbye() + break + + # Check if the user wants to reset application + if an_input in ("r", "reset") or t_controller.update_success: + ret_code = reset(t_controller.queue if t_controller.queue else []) + if ret_code != 0: + print_goodbye() + break + + except SystemExit: + logger.exception( + "The command '%s' doesn't exist on the / menu.", + an_input, + ) + console.print( + f"\nThe command '{an_input}' doesn't exist on the / menu", end="" + ) + similar_cmd = difflib.get_close_matches( + an_input.split(" ")[0] if " " in an_input else an_input, + t_controller.controller_choices, + n=1, + cutoff=0.7, + ) + if similar_cmd: + if " " in an_input: + candidate_input = ( + f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}" + ) + if candidate_input == an_input: + an_input = "" + t_controller.queue = [] + console.print("\n") + continue + an_input = candidate_input + else: + an_input = similar_cmd[0] + + console.print(f" Replacing by '{an_input}'.") + t_controller.queue.insert(0, an_input) + else: + console.print("\n") + + +def insert_start_slash(cmds: List[str]) -> List[str]: + if not cmds[0].startswith("/"): + cmds[0] = f"/{cmds[0]}" + if cmds[0].startswith("/home"): + cmds[0] = f"/{cmds[0][5:]}" + return cmds + + +def do_rollover(): + """RollOver the log file.""" + for handler in logging.getLogger().handlers: + if isinstance(handler, PathTrackingFileHandler): + handler.doRollover() + + +def log_settings() -> None: + """Log settings""" + settings_dict = {} + settings_dict["tab"] = "True" if obbff.USE_TABULATE_DF else "False" + settings_dict["cls"] = "True" if obbff.USE_CLEAR_AFTER_CMD else "False" + settings_dict["color"] = "True" if obbff.USE_COLOR else "False" + settings_dict["promptkit"] = "True" if obbff.USE_PROMPT_TOOLKIT else "False" + settings_dict["thoughts"] = "True" if obbff.ENABLE_THOUGHTS_DAY else "False" + settings_dict["reporthtml"] = "True" if obbff.OPEN_REPORT_AS_HTML else "False" + settings_dict["exithelp"] = "True" if obbff.ENABLE_EXIT_AUTO_HELP else "False" + settings_dict["rcontext"] = "True" if obbff.REMEMBER_CONTEXTS else "False" + settings_dict["rich"] = "True" if obbff.ENABLE_RICH else "False" + settings_dict["richpanel"] = "True" if obbff.ENABLE_RICH_PANEL else "False" + settings_dict["ion"] = "True" if obbff.USE_ION else "False" + settings_dict["watermark"] = "True" if obbff.USE_WATERMARK else "False" + settings_dict["autoscaling"] = "True" if obbff.USE_PLOT_AUTOSCALING else "False" + settings_dict["dt"] = "True" if obbff.USE_DATETIME else "False" + settings_dict["packaged"] = "True" if obbff.PACKAGED_APPLICATION else "False" + settings_dict["python"] = str(platform.python_version()) + settings_dict["os"] = str(platform.system()) + + logger.info("SETTINGS: %s ", str(settings_dict)) + + do_rollover() + + +def run_scripts( + path: str, + test_mode: bool = False, + verbose: bool = False, + routines_args: List[str] = None, +): + """Runs a given .openbb scripts + + Parameters + ---------- + path : str + The location of the .openbb file + test_mode : bool + Whether the terminal is in test mode + verbose : bool + Whether to run tests in verbose mode + routines_args : List[str] + One or multiple inputs to be replaced in the routine and separated by commas. + E.g. GME,AMC,BTC-USD + """ + if os.path.isfile(path): + with open(path) as fp: + raw_lines = [x for x in fp if (not is_reset(x)) and ("#" not in x) and x] + raw_lines = [ + raw_line.strip("\n") for raw_line in raw_lines if raw_line.strip("\n") + ] + + if routines_args: + lines = list() + for rawline in raw_lines: + templine = rawline + for i, arg in enumerate(routines_args): + templine = templine.replace(f"$ARGV[{i}]", arg) + lines.append(templine) + else: + lines = raw_lines + + if test_mode and "exit" not in lines[-1]: + lines.append("exit") + + export_folder = "" + if "export" in lines[0]: + export_folder = lines[0].split("export ")[1].rstrip() + lines = lines[1:] + + simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}" + file_cmds = simulate_argv.replace("//", "/home/").split() + file_cmds = insert_start_slash(file_cmds) if file_cmds else file_cmds + if export_folder: + file_cmds = [f"export {export_folder}{' '.join(file_cmds)}"] + else: + file_cmds = [" ".join(file_cmds)] + + if not test_mode: + terminal(file_cmds, appName="openbb_script") + # TODO: Add way to track how many commands are tested + else: + if verbose: + terminal(file_cmds, appName="openbb_script") + else: + with suppress_stdout(): + terminal(file_cmds, appName="openbb_script") + + else: + console.print(f"File '{path}' doesn't exist. Launching base terminal.\n") + if not test_mode: + terminal() + + +def main( + debug: bool, + test: bool, + filtert: str, + paths: List[str], + verbose: bool, + routines_args: List[str] = None, +): + """ + Runs the terminal with various options + + Parameters + ---------- + debug : bool + Whether to run the terminal in debug mode + test : bool + Whether to run the terminal in integrated test mode + filtert : str + Filter test files with given string in name + paths : List[str] + The paths to run for scripts or to test + verbose : bool + Whether to show output from tests + routines_args : List[str] + One or multiple inputs to be replaced in the routine and separated by commas. + E.g. GME,AMC,BTC-USD + """ + + if test: + os.environ["DEBUG_MODE"] = "true" + + if paths == "": + console.print("Please send a path when using test mode") + return + test_files = [] + for path in paths: + if path.endswith(".openbb"): + file = str(REPOSITORY_DIRECTORY / path) + test_files.append(file) + else: + folder = str(REPOSITORY_DIRECTORY / path) + files = [ + f"{folder}/{name}" + for name in os.listdir(folder) + if os.path.isfile(os.path.join(folder, name)) + and name.endswith(".openbb") + and (filtert in f"{folder}/{name}") + ] + test_files += files + test_files.sort() + SUCCESSES = 0 + FAILURES = 0 + fails = {} + length = len(test_files) + i = 0 + console.print("[green]OpenBB Terminal Integrated Tests:\n[/green]") + for file in test_files: + file = file.replace("//", "/") + repo_path_position = file.rfind(REPOSITORY_DIRECTORY.name) + if repo_path_position >= 0: + file_name = file[repo_path_position:].replace("\\", "/") + else: + file_name = file + console.print(f"{file_name} {((i/length)*100):.1f}%") + try: + if not os.path.isfile(file): + raise ValueError("Given file does not exist") + run_scripts(file, test_mode=True, verbose=verbose) + SUCCESSES += 1 + except Exception as e: + fails[file] = e + FAILURES += 1 + i += 1 + if fails: + console.print("\n[red]Failures:[/red]\n") + for key, value in fails.items(): + repo_path_position = key.rfind(REPOSITORY_DIRECTORY.name) + if repo_path_position >= 0: + file_name = key[repo_path_position:].replace("\\", "/") + else: + file_name = key + logger.error("%s: %s failed", file_name, value) + console.print(f"{file_name}: {value}\n") + console.print( + f"Summary: [green]Successes: {SUCCESSES}[/green] [red]Failures: {FAILURES}[/red]" + ) + return + + if debug: + os.environ["DEBUG_MODE"] = "true" + if isinstance(paths, list) and paths[0].endswith(".openbb"): + run_scripts(paths[0], routines_args=routines_args) + elif paths: + argv_cmds = list([" ".join(paths).replace(" /", "/home/")]) + argv_cmds = insert_start_slash(argv_cmds) if argv_cmds else argv_cmds + terminal(argv_cmds) + else: + terminal() + + +def parse_args_and_run(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="terminal", + description="The OpenBB Terminal.", + ) + parser.add_argument( + "-d", + "--debug", + dest="debug", + action="store_true", + default=False, + help="Runs the terminal in debug mode.", + ) + parser.add_argument( + "--file", + help="The path or .openbb file to run.", + dest="path", + nargs="+", + default="", + type=str, + ) + parser.add_argument( + "-t", + "--test", + dest="test", + action="store_true", + default=False, + help="Whether to run in test mode.", + ) + parser.add_argument( + "--filter", + help="Send a keyword to filter in file name", + dest="filtert", + default="", + type=str, + ) + parser.add_argument( + "-v", "--verbose", dest="verbose", action="store_true", default=False + ) + parser.add_argument( + "-i", + "--input", + help=( + "Select multiple inputs to be replaced in the routine and separated by commas." + "E.g. GME,AMC,BTC-USD" + ), + dest="routine_args", + type=lambda s: [str(item) for item in s.split(",")], + default=None, + ) + + if sys.argv[1:] and "-" not in sys.argv[1][0]: + sys.argv.insert(1, "--file") + ns_parser = parser.parse_args() + main( + ns_parser.debug, + ns_parser.test, + ns_parser.filtert, + ns_parser.path, + ns_parser.verbose, + ns_parser.routine_args, + ) + + +if __name__ == "__main__": + parse_args_and_run() diff --git a/terminal.py b/terminal.py index e5b9c78e65f..48d3909ee6d 100644 --- a/terminal.py +++ b/terminal.py @@ -1,1027 +1,5 @@ -#!/usr/bin/env python -"""Main Terminal Module""" -__docformat__ = "numpy" - -import argparse -import difflib -import logging -import os -import platform -import sys -import webbrowser -from typing import List -import dotenv - -from prompt_toolkit import PromptSession -from prompt_toolkit.completion import NestedCompleter -from prompt_toolkit.styles import Style -from prompt_toolkit.formatted_text import HTML -import pandas as pd - -from openbb_terminal import feature_flags as obbff - -from openbb_terminal.core.config.paths import ( - REPOSITORY_DIRECTORY, - USER_ENV_FILE, - REPOSITORY_ENV_FILE, - HOME_DIRECTORY, - ROUTINES_DIRECTORY, -) -from openbb_terminal.core.log.generation.path_tracking_file_handler import ( - PathTrackingFileHandler, -) - -from openbb_terminal.helper_funcs import ( - check_positive, - get_flair, - parse_simple_args, - EXPORT_ONLY_RAW_DATA_ALLOWED, -) -from openbb_terminal.loggers import setup_logging -from openbb_terminal.menu import session -from openbb_terminal.parent_classes import BaseController -from openbb_terminal.rich_config import console, MenuText, translate -from openbb_terminal.terminal_helper import ( - bootup, - check_for_updates, - is_reset, - print_goodbye, - reset, - suppress_stdout, - update_terminal, - welcome_message, -) -from openbb_terminal.helper_funcs import parse_and_split_input -from openbb_terminal.common import feedparser_view - -# pylint: disable=too-many-public-methods,import-outside-toplevel,too-many-branches,no-member,C0302 - -logger = logging.getLogger(__name__) - -env_file = str(USER_ENV_FILE) - - -class TerminalController(BaseController): - """Terminal Controller class""" - - CHOICES_COMMANDS = [ - "keys", - "settings", - "survey", - "update", - "featflags", - "exe", - "guess", - "news", - ] - CHOICES_MENUS = [ - "stocks", - "economy", - "crypto", - "portfolio", - "forex", - "etf", - "reports", - "dashboards", - "funds", - "alternative", - "econometrics", - "sources", - "forecast", - ] - - PATH = "/" - - GUESS_TOTAL_TRIES = 0 - GUESS_NUMBER_TRIES_LEFT = 0 - GUESS_SUM_SCORE = 0.0 - GUESS_CORRECTLY = 0 - - def __init__(self, jobs_cmds: List[str] = None): - """Constructor""" - super().__init__(jobs_cmds) - - self.queue: List[str] = list() - - if jobs_cmds: - self.queue = parse_and_split_input( - an_input=" ".join(jobs_cmds), custom_filters=[] - ) - - self.update_success = False - - self.update_runtime_choices() - - def update_runtime_choices(self): - """Update runtime choices""" - self.ROUTINE_FILES = { - filepath.name: filepath - for filepath in (REPOSITORY_DIRECTORY / "routines").rglob("*.openbb") - } - self.ROUTINE_FILES.update( - { - filepath.name: filepath - for filepath in ROUTINES_DIRECTORY.rglob("*.openbb") - } - ) - self.ROUTINE_CHOICES = {filename: None for filename in self.ROUTINE_FILES} - if session and obbff.USE_PROMPT_TOOLKIT: - choices: dict = {c: {} for c in self.controller_choices} - choices["support"] = self.SUPPORT_CHOICES - choices["exe"] = self.ROUTINE_CHOICES - - self.completer = NestedCompleter.from_nested_dict(choices) - - def print_help(self): - """Print help""" - mt = MenuText("") - mt.add_info("_home_") - mt.add_cmd("about") - mt.add_cmd("support") - mt.add_cmd("survey") - mt.add_cmd("update") - mt.add_cmd("wiki") - mt.add_cmd("record") - mt.add_cmd("stop") - mt.add_raw("\n") - mt.add_info("_configure_") - mt.add_menu("keys") - mt.add_menu("featflags") - mt.add_menu("sources") - mt.add_menu("settings") - mt.add_raw("\n") - mt.add_cmd("news") - mt.add_cmd("exe") - mt.add_raw("\n") - mt.add_info("_main_menu_") - mt.add_menu("stocks") - mt.add_menu("crypto") - mt.add_menu("etf") - mt.add_menu("economy") - mt.add_menu("forex") - mt.add_menu("funds") - mt.add_menu("alternative") - mt.add_raw("\n") - mt.add_info("_others_") - mt.add_menu("econometrics") - mt.add_menu("forecast") - mt.add_menu("portfolio") - mt.add_menu("dashboards") - mt.add_menu("reports") - mt.add_raw("\n") - console.print(text=mt.menu_text, menu="Home") - self.update_runtime_choices() - - def call_news(self, other_args: List[str]) -> None: - """Process news command""" - parse = argparse.ArgumentParser( - add_help=False, - prog="news", - description=translate("news"), - ) - parse.add_argument( - "-t", - "--term", - dest="term", - default=[""], - nargs="+", - help="search for a term on the news", - ) - parse.add_argument( - "-s", - "--sources", - dest="sources", - default=["bloomberg.com"], - nargs="+", - help="sources from where to get news from", - ) - if other_args and "-" not in other_args[0][0]: - other_args.insert(0, "-t") - news_parser = self.parse_known_args_and_warn( - parse, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED, limit=5 - ) - if news_parser: - feedparser_view.display_news( - term=" ".join(news_parser.term), - sources=" ".join(news_parser.sources), - limit=news_parser.limit, - export=news_parser.export, - ) - - def call_guess(self, other_args: List[str]) -> None: - """Process guess command""" - import time - import json - import random - - if self.GUESS_NUMBER_TRIES_LEFT == 0 and self.GUESS_SUM_SCORE < 0.01: - parser_exe = argparse.ArgumentParser( - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="guess", - description="Guess command to achieve task successfully.", - ) - parser_exe.add_argument( - "-l", - "--limit", - type=check_positive, - help="Number of tasks to attempt.", - dest="limit", - default=1, - ) - if other_args and "-" not in other_args[0][0]: - other_args.insert(0, "-l") - ns_parser_guess = parse_simple_args(parser_exe, other_args) - - if self.GUESS_TOTAL_TRIES == 0: - self.GUESS_NUMBER_TRIES_LEFT = ns_parser_guess.limit - self.GUESS_SUM_SCORE = 0 - self.GUESS_TOTAL_TRIES = ns_parser_guess.limit - - try: - with open(obbff.GUESS_EASTER_EGG_FILE) as f: - # Load the file as a JSON document - json_doc = json.load(f) - - task = random.choice(list(json_doc.keys())) # nosec - solution = json_doc[task] - - start = time.time() - console.print(f"\n[yellow]{task}[/yellow]\n") - if isinstance(session, PromptSession): - an_input = session.prompt("GUESS / $ ") - else: - an_input = "" - time_dif = time.time() - start - - # When there are multiple paths to same solution - if isinstance(solution, List): - if an_input.lower() in [s.lower() for s in solution]: - self.queue = an_input.split("/") + ["home"] - console.print( - f"\n[green]You guessed correctly in {round(time_dif, 2)} seconds![green]\n" - ) - # If we are already counting successes - if self.GUESS_TOTAL_TRIES > 0: - self.GUESS_CORRECTLY += 1 - self.GUESS_SUM_SCORE += time_dif - else: - solutions_texts = "\n".join(solution) - console.print( - f"\n[red]You guessed wrong! The correct paths would have been:\n{solutions_texts}[/red]\n" - ) - - # When there is a single path to the solution - else: - if an_input.lower() == solution.lower(): - self.queue = an_input.split("/") + ["home"] - console.print( - f"\n[green]You guessed correctly in {round(time_dif, 2)} seconds![green]\n" - ) - # If we are already counting successes - if self.GUESS_TOTAL_TRIES > 0: - self.GUESS_CORRECTLY += 1 - self.GUESS_SUM_SCORE += time_dif - else: - console.print( - f"\n[red]You guessed wrong! The correct path would have been:\n{solution}[/red]\n" - ) - - # Compute average score and provide a result if it's the last try - if self.GUESS_TOTAL_TRIES > 0: - - self.GUESS_NUMBER_TRIES_LEFT -= 1 - if self.GUESS_NUMBER_TRIES_LEFT == 0 and self.GUESS_TOTAL_TRIES > 1: - color = ( - "green" - if self.GUESS_CORRECTLY == self.GUESS_TOTAL_TRIES - else "red" - ) - console.print( - f"[{color}]OUTCOME: You got {int(self.GUESS_CORRECTLY)} out of" - f" {int(self.GUESS_TOTAL_TRIES)}.[/{color}]\n" - ) - if self.GUESS_CORRECTLY == self.GUESS_TOTAL_TRIES: - avg = self.GUESS_SUM_SCORE / self.GUESS_TOTAL_TRIES - console.print( - f"[green]Average score: {round(avg, 2)} seconds![/green]\n" - ) - self.GUESS_TOTAL_TRIES = 0 - self.GUESS_CORRECTLY = 0 - self.GUESS_SUM_SCORE = 0 - else: - self.queue += ["guess"] - - except Exception as e: - console.print( - f"[red]Failed to load guess game from file: " - f"{obbff.GUESS_EASTER_EGG_FILE}[/red]" - ) - console.print(f"[red]{e}[/red]") - - @staticmethod - def call_survey(_) -> None: - """Process survey command""" - webbrowser.open("https://openbb.co/survey") - - def call_update(self, _): - """Process update command""" - if not obbff.PACKAGED_APPLICATION: - self.update_success = not update_terminal() - else: - console.print( - "Find the most recent release of the OpenBB Terminal here: " - "https://openbb.co/products/terminal#get-started\n" - ) - - def call_keys(self, _): - """Process keys command""" - from openbb_terminal.keys_controller import KeysController - - self.queue = self.load_class(KeysController, self.queue, env_file) - - def call_settings(self, _): - """Process settings command""" - from openbb_terminal.settings_controller import SettingsController - - self.queue = self.load_class(SettingsController, self.queue, env_file) - - def call_featflags(self, _): - """Process feature flags command""" - from openbb_terminal.featflags_controller import FeatureFlagsController - - self.queue = self.load_class(FeatureFlagsController, self.queue) - - def call_stocks(self, _): - """Process stocks command""" - from openbb_terminal.stocks.stocks_controller import StocksController - - self.queue = self.load_class(StocksController, self.queue) - - def call_crypto(self, _): - """Process crypto command""" - from openbb_terminal.cryptocurrency.crypto_controller import CryptoController - - self.queue = self.load_class(CryptoController, self.queue) - - def call_economy(self, _): - """Process economy command""" - from openbb_terminal.economy.economy_controller import EconomyController - - self.queue = self.load_class(EconomyController, self.queue) - - def call_etf(self, _): - """Process etf command""" - from openbb_terminal.etf.etf_controller import ETFController - - self.queue = self.load_class(ETFController, self.queue) - - def call_funds(self, _): - """Process etf command""" - from openbb_terminal.mutual_funds.mutual_fund_controller import ( - FundController, - ) - - self.queue = self.load_class(FundController, self.queue) - - def call_forex(self, _): - """Process forex command""" - from openbb_terminal.forex.forex_controller import ForexController - - self.queue = self.load_class(ForexController, self.queue) - - def call_reports(self, _): - """Process reports command""" - from openbb_terminal.reports.reports_controller import ( - ReportController, - ) - - self.queue = self.load_class(ReportController, self.queue) - - def call_dashboards(self, _): - """Process dashboards command""" - if not obbff.PACKAGED_APPLICATION: - from openbb_terminal.dashboards.dashboards_controller import ( - DashboardsController, - ) - - self.queue = self.load_class(DashboardsController, self.queue) - else: - console.print("This feature is coming soon.") - console.print( - "Use the source code and an Anaconda environment if you are familiar with Python." - ) - - def call_alternative(self, _): - """Process alternative command""" - from openbb_terminal.alternative.alt_controller import ( - AlternativeDataController, - ) - - self.queue = self.load_class(AlternativeDataController, self.queue) - - def call_econometrics(self, _): - """Process econometrics command""" - from openbb_terminal.econometrics.econometrics_controller import ( - EconometricsController, - ) - - self.queue = EconometricsController(self.queue).menu() - - def call_forecast(self, _): - """Process forecast command""" - from openbb_terminal.forecast.forecast_controller import ( - ForecastController, - ) - - self.queue = self.load_class(ForecastController, "", pd.DataFrame(), self.queue) - - def call_portfolio(self, _): - """Process portfolio command""" - from openbb_terminal.portfolio.portfolio_controller import ( - PortfolioController, - ) - - self.queue = self.load_class(PortfolioController, self.queue) - - def call_sources(self, _): - """Process sources command""" - from openbb_terminal.sources_controller import SourcesController - - self.queue = self.load_class(SourcesController, self.queue) - - def call_exe(self, other_args: List[str]): - """Process exe command""" - # Merge rest of string path to other_args and remove queue since it is a dir - other_args += self.queue - - if not other_args: - console.print( - "[red]Provide a path to the routine you wish to execute.\n[/red]" - ) - return - - full_input = " ".join(other_args) - if " " in full_input: - other_args_processed = full_input.split(" ") - else: - other_args_processed = [full_input] - self.queue = [] - - path_routine = "" - args = list() - for idx, path_dir in enumerate(other_args_processed): - if path_dir in ("-i", "--input"): - args = [path_routine[1:]] + other_args_processed[idx:] - break - if path_dir not in ("--file"): - path_routine += f"/{path_dir}" - - if not args: - args = [path_routine[1:]] - - parser_exe = argparse.ArgumentParser( - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="exe", - description="Execute automated routine script.", - ) - parser_exe.add_argument( - "--file", - help="The path or .openbb file to run.", - dest="path", - default="", - required="-h" not in args, - ) - parser_exe.add_argument( - "-i", - "--input", - help="Select multiple inputs to be replaced in the routine and separated by commas. E.g. GME,AMC,BTC-USD", - dest="routine_args", - type=lambda s: [str(item) for item in s.split(",")], - ) - if args and "-" not in args[0][0]: - args.insert(0, "--file") - ns_parser_exe = parse_simple_args(parser_exe, args) - if ns_parser_exe: - if ns_parser_exe.path: - if ns_parser_exe.path in self.ROUTINE_CHOICES: - path = self.ROUTINE_FILES[ns_parser_exe.path] - else: - path = ns_parser_exe.path - - with open(path) as fp: - raw_lines = [ - x for x in fp if (not is_reset(x)) and ("#" not in x) and x - ] - raw_lines = [ - raw_line.strip("\n") - for raw_line in raw_lines - if raw_line.strip("\n") - ] - - lines = list() - for rawline in raw_lines: - templine = rawline - - # Check if dynamic parameter exists in script - if "$ARGV" in rawline: - # Check if user has provided inputs through -i or --input - if ns_parser_exe.routine_args: - for i, arg in enumerate(ns_parser_exe.routine_args): - # Check what is the location of the ARGV to be replaced - if f"$ARGV[{i}]" in templine: - templine = templine.replace(f"$ARGV[{i}]", arg) - - # Check if all ARGV have been removed, otherwise means that there are less inputs - # when running the script than the script expects - if "$ARGV" in templine: - console.print( - "[red]Not enough inputs were provided to fill in dynamic variables. " - "E.g. --input VAR1,VAR2,VAR3[/red]\n" - ) - return - - lines.append(templine) - # The script expects a parameter that the user has not provided - else: - console.print( - "[red]The script expects parameters, " - "run the script again with --input defined.[/red]\n" - ) - return - else: - lines.append(templine) - - simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}" - file_cmds = simulate_argv.replace("//", "/home/").split() - file_cmds = ( - insert_start_slash(file_cmds) if file_cmds else file_cmds - ) - cmds_with_params = " ".join(file_cmds) - self.queue = [ - val - for val in parse_and_split_input( - an_input=cmds_with_params, custom_filters=[] - ) - if val - ] - - if "export" in self.queue[0]: - export_path = self.queue[0].split(" ")[1] - # If the path selected does not start from the user root, give relative location from root - if export_path[0] == "~": - export_path = export_path.replace( - "~", HOME_DIRECTORY.as_posix() - ) - elif export_path[0] != "/": - export_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), export_path - ) - - # Check if the directory exists - if os.path.isdir(export_path): - console.print( - f"Export data to be saved in the selected folder: '{export_path}'" - ) - else: - os.makedirs(export_path) - console.print( - f"[green]Folder '{export_path}' successfully created.[/green]" - ) - obbff.EXPORT_FOLDER_PATH = export_path - self.queue = self.queue[1:] - - -# pylint: disable=global-statement -def terminal(jobs_cmds: List[str] = None, appName: str = "gst"): - """Terminal Menu""" - # TODO: HELP WANTED! Refactor the appName setting if a more elegant solution comes up - if obbff.PACKAGED_APPLICATION: - appName = "gst_packaged" - - setup_logging(appName) - logger.info("START") - log_settings() - - if jobs_cmds is not None and jobs_cmds: - logger.info("INPUT: %s", "/".join(jobs_cmds)) - - export_path = "" - if jobs_cmds and "export" in jobs_cmds[0]: - export_path = jobs_cmds[0].split("/")[0].split(" ")[1] - jobs_cmds = ["/".join(jobs_cmds[0].split("/")[1:])] - - ret_code = 1 - t_controller = TerminalController(jobs_cmds) - an_input = "" - - if export_path: - # If the path selected does not start from the user root, give relative location from terminal root - if export_path[0] == "~": - export_path = export_path.replace("~", HOME_DIRECTORY.as_posix()) - elif export_path[0] != "/": - export_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), export_path - ) - - # Check if the directory exists - if os.path.isdir(export_path): - console.print( - f"Export data to be saved in the selected folder: '{export_path}'" - ) - else: - os.makedirs(export_path) - console.print( - f"[green]Folder '{export_path}' successfully created.[/green]" - ) - obbff.EXPORT_FOLDER_PATH = export_path - - bootup() - if not jobs_cmds: - welcome_message() - t_controller.print_help() - check_for_updates() - - dotenv.load_dotenv(USER_ENV_FILE) - dotenv.load_dotenv(REPOSITORY_ENV_FILE, override=True) - - while ret_code: - if obbff.ENABLE_QUICK_EXIT: - console.print("Quick exit enabled") - break - - # There is a command in the queue - if t_controller.queue and len(t_controller.queue) > 0: - # If the command is quitting the menu we want to return in here - if t_controller.queue[0] in ("q", "..", "quit"): - print_goodbye() - break - - # Consume 1 element from the queue - an_input = t_controller.queue[0] - t_controller.queue = t_controller.queue[1:] - - # Print the current location because this was an instruction and we want user to know what was the action - if an_input and an_input.split(" ")[0] in t_controller.CHOICES_COMMANDS: - console.print(f"{get_flair()} / $ {an_input}") - - # Get input command from user - else: - # Get input from user using auto-completion - if session and obbff.USE_PROMPT_TOOLKIT: - try: - if obbff.TOOLBAR_HINT: - an_input = session.prompt( - f"{get_flair()} / $ ", - completer=t_controller.completer, - search_ignore_case=True, - bottom_toolbar=HTML( - ' help menu ' - ' return to previous menu ' - ' exit terminal ' - ' ' - "see usage and available options " - ' Getting Started Documentation' - ), - style=Style.from_dict( - { - "bottom-toolbar": "#ffffff bg:#333333", - } - ), - ) - else: - an_input = session.prompt( - f"{get_flair()} / $ ", - completer=t_controller.completer, - search_ignore_case=True, - ) - except (KeyboardInterrupt, EOFError): - print_goodbye() - break - # Get input from user without auto-completion - else: - an_input = input(f"{get_flair()} / $ ") - - try: - # Process the input command - t_controller.queue = t_controller.switch(an_input) - if an_input in ("q", "quit", "..", "exit"): - print_goodbye() - break - - # Check if the user wants to reset application - if an_input in ("r", "reset") or t_controller.update_success: - ret_code = reset(t_controller.queue if t_controller.queue else []) - if ret_code != 0: - print_goodbye() - break - - except SystemExit: - logger.exception( - "The command '%s' doesn't exist on the / menu.", - an_input, - ) - console.print( - f"\nThe command '{an_input}' doesn't exist on the / menu", end="" - ) - similar_cmd = difflib.get_close_matches( - an_input.split(" ")[0] if " " in an_input else an_input, - t_controller.controller_choices, - n=1, - cutoff=0.7, - ) - if similar_cmd: - if " " in an_input: - candidate_input = ( - f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}" - ) - if candidate_input == an_input: - an_input = "" - t_controller.queue = [] - console.print("\n") - continue - an_input = candidate_input - else: - an_input = similar_cmd[0] - - console.print(f" Replacing by '{an_input}'.") - t_controller.queue.insert(0, an_input) - else: - console.print("\n") - - -def insert_start_slash(cmds: List[str]) -> List[str]: - if not cmds[0].startswith("/"): - cmds[0] = f"/{cmds[0]}" - if cmds[0].startswith("/home"): - cmds[0] = f"/{cmds[0][5:]}" - return cmds - - -def do_rollover(): - """RollOver the log file.""" - for handler in logging.getLogger().handlers: - if isinstance(handler, PathTrackingFileHandler): - handler.doRollover() - - -def log_settings() -> None: - """Log settings""" - settings_dict = {} - settings_dict["tab"] = "True" if obbff.USE_TABULATE_DF else "False" - settings_dict["cls"] = "True" if obbff.USE_CLEAR_AFTER_CMD else "False" - settings_dict["color"] = "True" if obbff.USE_COLOR else "False" - settings_dict["promptkit"] = "True" if obbff.USE_PROMPT_TOOLKIT else "False" - settings_dict["thoughts"] = "True" if obbff.ENABLE_THOUGHTS_DAY else "False" - settings_dict["reporthtml"] = "True" if obbff.OPEN_REPORT_AS_HTML else "False" - settings_dict["exithelp"] = "True" if obbff.ENABLE_EXIT_AUTO_HELP else "False" - settings_dict["rcontext"] = "True" if obbff.REMEMBER_CONTEXTS else "False" - settings_dict["rich"] = "True" if obbff.ENABLE_RICH else "False" - settings_dict["richpanel"] = "True" if obbff.ENABLE_RICH_PANEL else "False" - settings_dict["ion"] = "True" if obbff.USE_ION else "False" - settings_dict["watermark"] = "True" if obbff.USE_WATERMARK else "False" - settings_dict["autoscaling"] = "True" if obbff.USE_PLOT_AUTOSCALING else "False" - settings_dict["dt"] = "True" if obbff.USE_DATETIME else "False" - settings_dict["packaged"] = "True" if obbff.PACKAGED_APPLICATION else "False" - settings_dict["python"] = str(platform.python_version()) - settings_dict["os"] = str(platform.system()) - - logger.info("SETTINGS: %s ", str(settings_dict)) - - do_rollover() - - -def run_scripts( - path: str, - test_mode: bool = False, - verbose: bool = False, - routines_args: List[str] = None, -): - """Runs a given .openbb scripts - - Parameters - ---------- - path : str - The location of the .openbb file - test_mode : bool - Whether the terminal is in test mode - verbose : bool - Whether to run tests in verbose mode - routines_args : List[str] - One or multiple inputs to be replaced in the routine and separated by commas. - E.g. GME,AMC,BTC-USD - """ - if os.path.isfile(path): - with open(path) as fp: - raw_lines = [x for x in fp if (not is_reset(x)) and ("#" not in x) and x] - raw_lines = [ - raw_line.strip("\n") for raw_line in raw_lines if raw_line.strip("\n") - ] - - if routines_args: - lines = list() - for rawline in raw_lines: - templine = rawline - for i, arg in enumerate(routines_args): - templine = templine.replace(f"$ARGV[{i}]", arg) - lines.append(templine) - else: - lines = raw_lines - - if test_mode and "exit" not in lines[-1]: - lines.append("exit") - - export_folder = "" - if "export" in lines[0]: - export_folder = lines[0].split("export ")[1].rstrip() - lines = lines[1:] - - simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}" - file_cmds = simulate_argv.replace("//", "/home/").split() - file_cmds = insert_start_slash(file_cmds) if file_cmds else file_cmds - if export_folder: - file_cmds = [f"export {export_folder}{' '.join(file_cmds)}"] - else: - file_cmds = [" ".join(file_cmds)] - - if not test_mode: - terminal(file_cmds, appName="openbb_script") - # TODO: Add way to track how many commands are tested - else: - if verbose: - terminal(file_cmds, appName="openbb_script") - else: - with suppress_stdout(): - terminal(file_cmds, appName="openbb_script") - - else: - console.print(f"File '{path}' doesn't exist. Launching base terminal.\n") - if not test_mode: - terminal() - - -def main( - debug: bool, - test: bool, - filtert: str, - paths: List[str], - verbose: bool, - routines_args: List[str] = None, -): - """ - Runs the terminal with various options - - Parameters - ---------- - debug : bool - Whether to run the terminal in debug mode - test : bool - Whether to run the terminal in integrated test mode - filtert : str - Filter test files with given string in name - paths : List[str] - The paths to run for scripts or to test - verbose : bool - Whether to show output from tests - routines_args : List[str] - One or multiple inputs to be replaced in the routine and separated by commas. - E.g. GME,AMC,BTC-USD - """ - - if test: - os.environ["DEBUG_MODE"] = "true" - - if paths == []: - console.print("Please send a path when using test mode") - return - test_files = [] - for path in paths: - if path.endswith(".openbb"): - file = os.path.join(os.path.abspath(os.path.dirname(__file__)), path) - test_files.append(file) - else: - folder = os.path.join(os.path.abspath(os.path.dirname(__file__)), path) - files = [ - f"{folder}/{name}" - for name in os.listdir(folder) - if os.path.isfile(os.path.join(folder, name)) - and name.endswith(".openbb") - and (filtert in f"{folder}/{name}") - ] - test_files += files - test_files.sort() - SUCCESSES = 0 - FAILURES = 0 - fails = {} - length = len(test_files) - i = 0 - console.print("[green]OpenBB Terminal Integrated Tests:\n[/green]") - for file in test_files: - file = file.replace("//", "/") - repo_path_position = file.rfind(REPOSITORY_DIRECTORY.name) - if repo_path_position >= 0: - file_name = file[repo_path_position:].replace("\\", "/") - else: - file_name = file - console.print(f"{file_name} {((i/length)*100):.1f}%") - try: - if not os.path.isfile(file): - raise ValueError("Given file does not exist") - run_scripts(file, test_mode=True, verbose=verbose) - SUCCESSES += 1 - except Exception as e: - fails[file] = e - FAILURES += 1 - i += 1 - if fails: - console.print("\n[red]Failures:[/red]\n") - for key, value in fails.items(): - repo_path_position = key.rfind(REPOSITORY_DIRECTORY.name) - if repo_path_position >= 0: - file_name = key[repo_path_position:].replace("\\", "/") - else: - file_name = key - logger.error("%s: %s failed", file_name, value) - console.print(f"{file_name}: {value}\n") - console.print( - f"Summary: [green]Successes: {SUCCESSES}[/green] [red]Failures: {FAILURES}[/red]" - ) - return - - if debug: - os.environ["DEBUG_MODE"] = "true" - if isinstance(paths, list) and paths[0].endswith(".openbb"): - run_scripts(paths[0], routines_args=routines_args) - elif paths: - argv_cmds = list([" ".join(paths).replace(" /", "/home/")]) - argv_cmds = insert_start_slash(argv_cmds) if argv_cmds else argv_cmds - terminal(argv_cmds) - else: - terminal() +from openbb_terminal.terminal_controller import parse_args_and_run if __name__ == "__main__": - - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="terminal", - description="The OpenBB Terminal.", - ) - parser.add_argument( - "-d", - "--debug", - dest="debug", - action="store_true", - default=False, - help="Runs the terminal in debug mode.", - ) - parser.add_argument( - "--file", - help="The path or .openbb file to run.", - dest="path", - nargs="+", - default="", - type=str, - ) - parser.add_argument( - "-t", - "--test", - dest="test", - action="store_true", - default=False, - help="Whether to run in test mode.", - ) - parser.add_argument( - "--filter", - help="Send a keyword to filter in file name", - dest="filtert", - default="", - type=str, - ) - parser.add_argument( - "-v", "--verbose", dest="verbose", action="store_true", default=False - ) - parser.add_argument( - "-i", - "--input", - help=( - "Select multiple inputs to be replaced in the routine and separated by commas." - "E.g. GME,AMC,BTC-USD" - ), - dest="routine_args", - type=lambda s: [str(item) for item in s.split(",")], - default=None, - ) - - if sys.argv[1:] and "-" not in sys.argv[1][0]: - sys.argv.insert(1, "--file") - ns_parser = parser.parse_args() - main( - ns_parser.debug, - ns_parser.test, - ns_parser.filtert, - ns_parser.path, - ns_parser.verbose, - ns_parser.routine_args, - ) + parse_args_and_run() diff --git a/tests/openbb_terminal/test_terminal_controller.py b/tests/openbb_terminal/test_terminal_controller.py new file mode 100644 index 00000000000..b98e8fdd750 --- /dev/null +++ b/tests/openbb_terminal/test_terminal_controller.py @@ -0,0 +1,60 @@ +from contextlib import contextmanager +import pytest +from openbb_terminal import terminal_controller + + +@pytest.mark.skip +@pytest.mark.block_network +@pytest.mark.vcr(record_mode="none") +@pytest.mark.record_stdout +def test_terminal_quick_exit(mocker, monkeypatch): + monkeypatch.setattr(terminal_controller.obbff, "ENABLE_QUICK_EXIT", True) + monkeypatch.setattr(terminal_controller.obbff, "USE_ION", False) + monkeypatch.setattr(terminal_controller.obbff, "USE_PROMPT_TOOLKIT", True) + + mocker.patch("sys.stdin") + + terminal_controller.terminal() + + +@pytest.mark.skip +@pytest.mark.vcr(record_mode="none") +@pytest.mark.record_stdout +def test_terminal_quit(mocker, monkeypatch): + monkeypatch.setattr(terminal_controller.obbff, "ENABLE_QUICK_EXIT", False) + monkeypatch.setattr(terminal_controller.obbff, "USE_ION", False) + monkeypatch.setattr(terminal_controller.obbff, "USE_PROMPT_TOOLKIT", True) + + mocker.patch("sys.stdin") + mocker.patch("builtins.input", return_value="e") + mocker.patch("terminal_controller.session") + mocker.patch("terminal_controller.session.prompt", return_value="e") + mocker.patch("terminal_controller.print_goodbye") + spy_print_goodbye = mocker.spy(terminal_controller, "print_goodbye") + + terminal_controller.terminal() + + spy_print_goodbye.assert_called_once() + + +@contextmanager +def no_suppress(): + yield + + +@pytest.mark.skip +@pytest.mark.vcr(record_mode="none") +@pytest.mark.record_stdout +@pytest.mark.parametrize( + "debug, test, verbose, filtert, path", + [ + (True, False, False, None, None), + (False, False, False, None, ["scripts/test_alt_covid.openbb"]), + # (False, True, False, "alt_covid", ["scripts/"]), + # (False, True, True, "alt_covid", ["scripts/"]), + ], +) +def test_menu(mocker, debug, test, filtert, path, verbose): + mocker.patch(target="terminal_controller.terminal") + mocker.patch(target="terminal_controller.suppress_stdout", side_effect=no_suppress) + terminal_controller.main(debug, test, filtert, path, verbose) diff --git a/tests/test_terminal.py b/tests/test_terminal.py deleted file mode 100644 index 9a299398a3b..00000000000 --- a/tests/test_terminal.py +++ /dev/null @@ -1,60 +0,0 @@ -from contextlib import contextmanager -import pytest -import terminal - - -@pytest.mark.skip -@pytest.mark.block_network -@pytest.mark.vcr(record_mode="none") -@pytest.mark.record_stdout -def test_terminal_quick_exit(mocker, monkeypatch): - monkeypatch.setattr(terminal.obbff, "ENABLE_QUICK_EXIT", True) - monkeypatch.setattr(terminal.obbff, "USE_ION", False) - monkeypatch.setattr(terminal.obbff, "USE_PROMPT_TOOLKIT", True) - - mocker.patch("sys.stdin") - - terminal.terminal() - - -@pytest.mark.skip -@pytest.mark.vcr(record_mode="none") -@pytest.mark.record_stdout -def test_terminal_quit(mocker, monkeypatch): - monkeypatch.setattr(terminal.obbff, "ENABLE_QUICK_EXIT", False) - monkeypatch.setattr(terminal.obbff, "USE_ION", False) - monkeypatch.setattr(terminal.obbff, "USE_PROMPT_TOOLKIT", True) - - mocker.patch("sys.stdin") - mocker.patch("builtins.input", return_value="e") - mocker.patch("terminal.session") - mocker.patch("terminal.session.prompt", return_value="e") - mocker.patch("terminal.print_goodbye") - spy_print_goodbye = mocker.spy(terminal, "print_goodbye") - - terminal.terminal() - - spy_print_goodbye.assert_called_once() - - -@contextmanager -def no_suppress(): - yield - - -@pytest.mark.skip -@pytest.mark.vcr(record_mode="none") -@pytest.mark.record_stdout -@pytest.mark.parametrize( - "debug, test, verbose, filtert, path", - [ - (True, False, False, None, None), - (False, False, False, None, ["scripts/test_alt_covid.openbb"]), - # (False, True, False, "alt_covid", ["scripts/"]), - # (False, True, True, "alt_covid", ["scripts/"]), - ], -) -def test_menu(mocker, debug, test, filtert, path, verbose): - mocker.patch(target="terminal.terminal") - mocker.patch(target="terminal.suppress_stdout", side_effect=no_suppress) - terminal.main(debug, test, filtert, path, verbose)