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)