"""Settings Controller Module""" __docformat__ = "numpy" # IMPORTATION STANDARD import argparse import logging import os import os.path from pathlib import Path from typing import List, Optional, Union # IMPORTATION THIRDPARTY import pytz import openbb_terminal.core.session.hub_model as Hub from openbb_terminal import theme # IMPORTATION INTERNAL from openbb_terminal.core.config.paths import ( I18N_DICT_LOCATION, SETTINGS_ENV_FILE, STYLES_DIRECTORY_REPO, ) from openbb_terminal.core.session.constants import BackendEnvironment from openbb_terminal.core.session.current_user import ( get_current_user, is_local, set_preference, ) from openbb_terminal.core.session.env_handler import write_to_dotenv from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_funcs import ( AVAILABLE_FLAIRS, get_flair, get_user_timezone_or_invalid, parse_and_split_input, ) from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import RICH_TAGS, MenuText, console # pylint: disable=too-many-lines,no-member,too-many-public-methods,C0302 # pylint: disable=import-outside-toplevel logger = logging.getLogger(__name__) class SettingsController(BaseController): """Settings Controller class""" CHOICES_COMMANDS: List[str] = [ "chart", "colors", "dt", "flair", "height", "lang", "table", "tz", "userdata", "width", ] if is_local(): CHOICES_COMMANDS.append("source") PATH = "/settings/" CHOICES_GENERATION = True languages_available = [ lang.strip(".yml") for lang in os.listdir(I18N_DICT_LOCATION) if lang.endswith(".yml") ] def __init__( self, queue: Optional[List[str]] = None, env_file: str = str(SETTINGS_ENV_FILE) ): """Constructor""" super().__init__(queue) self.env_file = env_file if session and get_current_user().preferences.USE_PROMPT_TOOLKIT: choices: dict = self.choices_default choices["tz"] = {c: None for c in pytz.all_timezones} choices["lang"] = {c: None for c in self.languages_available} choices["flair"]["--emoji"] = {c: None for c in AVAILABLE_FLAIRS} choices["flair"]["-e"] = "--emoji" self.choices = choices self.completer = NestedCompleter.from_nested_dict(choices) self.sort_filter = r"((tz\ -t |tz ).*?(" for tz in pytz.all_timezones: tz = tz.replace("/", r"\/") self.sort_filter += f"{tz}|" self.sort_filter += ")*)" self.PREVIEW = ", ".join( [ f"[{tag}]{tag}[/{tag}]" for tag in sorted( set( name.replace("[", "").replace("]", "").replace("/", "") for name in RICH_TAGS ) ) ] ) def parse_input(self, an_input: str) -> List: """Parse controller input Overrides the parent class function to handle github org/repo path convention. See `BaseController.parse_input()` for details. """ # Filtering out sort_filter = self.sort_filter custom_filters = [sort_filter] commands = parse_and_split_input( an_input=an_input, custom_filters=custom_filters ) return commands def print_help(self): """Print help""" current_user = get_current_user() mt = MenuText("settings/") mt.add_info("_info_") mt.add_raw("\n") mt.add_setting("dt", current_user.preferences.USE_DATETIME) mt.add_raw("\n") mt.add_cmd("chart") mt.add_raw("\n") mt.add_param("_chart", current_user.preferences.CHART_STYLE) mt.add_raw("\n") mt.add_cmd("table") mt.add_raw("\n") mt.add_param("_table", current_user.preferences.TABLE_STYLE) mt.add_raw("\n") mt.add_cmd("colors") mt.add_raw("\n") mt.add_param( "_colors", f"{current_user.preferences.RICH_STYLE} -> {self.PREVIEW}" ) mt.add_raw("\n") mt.add_cmd("flair") mt.add_raw("\n") mt.add_param("_flair", get_flair()) mt.add_raw("\n") mt.add_cmd("lang") mt.add_raw("\n") mt.add_param("_language", current_user.preferences.USE_LANGUAGE) mt.add_raw("\n") mt.add_cmd("userdata") mt.add_raw("\n") mt.add_param( "_user_data_folder", current_user.preferences.USER_DATA_DIRECTORY, ) mt.add_raw("\n") mt.add_cmd("tz") mt.add_raw("\n") mt.add_param("_timezone", get_user_timezone_or_invalid()) mt.add_raw("\n") mt.add_cmd("height") mt.add_cmd("width") mt.add_raw("\n") mt.add_param("_plot_height", current_user.preferences.PLOT_PYWRY_HEIGHT, 12) mt.add_param("_plot_width", current_user.preferences.PLOT_PYWRY_WIDTH, 12) mt.add_raw("\n") if is_local(): mt.add_cmd("source") mt.add_raw("\n") mt.add_param( "_data_source", get_current_user().preferences.USER_DATA_SOURCES_FILE, ) mt.add_raw("\n") console.print(text=mt.menu_text, menu="Settings") @staticmethod def set_and_save_preference(name: str, value: Union[bool, Path, str]): """Set preference and write to .env Parameters ---------- name : str Preference name value : Union[bool, Path, str] Preference value """ set_preference(name, value) write_to_dotenv("OPENBB_" + name, str(value)) @log_start_end(log=logger) def call_dt(self, other_args: List[str]): """Process dt command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="dt", description="Set the use of datetime in the plots", ) ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: self.set_and_save_preference( "USE_DATETIME", not get_current_user().preferences.USE_DATETIME ) @log_start_end(log=logger) def call_colors(self, other_args: List[str]): """Process colors command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="colors", description="Console style.", ) theme.load_available_styles() parser.add_argument( "-s", "--style", type=str, choices=theme.console_styles_available, dest="style", required="-h" not in other_args and "--help" not in other_args, help="To use 'custom' option, go to https://openbb.co/customize and create your theme." " Then, place the downloaded file 'openbb_config.richstyle.json'" f" inside {get_current_user().preferences.USER_STYLES_DIRECTORY} or " f"{STYLES_DIRECTORY_REPO}. If you have a hub account you can change colors " f"here {BackendEnvironment.BASE_URL + 'app/terminal/theme'}.", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-s") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: if is_local(): self.set_and_save_preference("RICH_STYLE", ns_parser.style) else: set_preference("RICH_STYLE", ns_parser.style) Hub.upload_config( key="RICH_STYLE", value=ns_parser.style, type_="settings", auth_header=get_current_user().profile.get_auth_header(), ) console.print("") console.print("Colors updated.") @log_start_end(log=logger) def call_chart(self, other_args: List[str]): """Process chart command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="theme", description="Choose chart style.", ) parser.add_argument( "-s", "--style", type=str, dest="style", choices=["dark", "light"], help="Choose chart style. If you have a hub account you can change theme " f"here {BackendEnvironment.BASE_URL + 'app/terminal/theme/charts-tables'}.", required="-h" not in other_args and "--help" not in other_args, ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-s") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser and ns_parser.style: if is_local(): self.set_and_save_preference("CHART_STYLE", ns_parser.style) else: set_preference("CHART_STYLE", ns_parser.style) Hub.upload_config( key="chart", value=ns_parser.style, type_="terminal_style", auth_header=get_current_user().profile.get_auth_header(), ) console.print("") theme.apply_style(ns_parser.style) console.print("Chart theme updated.\n") @log_start_end(log=logger) def call_table(self, other_args: List[str]): """Process theme command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="theme", description="Choose table style.", ) parser.add_argument( "-s", "--style", type=str, dest="style", choices=["dark", "light"], help="Choose table style. If you have a hub account you can change theme " f"here {BackendEnvironment.BASE_URL + 'app/terminal/theme/charts-tables'}.", required="-h" not in other_args and "--help" not in other_args, ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-s") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser and ns_parser.style: if is_local(): self.set_and_save_preference("TABLE_STYLE", ns_parser.style) else: set_preference("TABLE_STYLE", ns_parser.style) Hub.upload_config( key="table", value=ns_parser.style, type_="terminal_style", auth_header=get_current_user().profile.get_auth_header(), ) console.print("") console.print("Table theme updated.\n") @log_start_end(log=logger) def call_source(self, other_args: List[str]): """Process source command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="source", description="Preferred data source file.", ) parser.add_argument( "-f", "--file", type=str, dest="file", help="file", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-f") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser and ns_parser.file: if os.path.exists(ns_parser.file): self.set_and_save_preference( "USER_DATA_SOURCES_FILE", str(ns_parser.file) ) console.print("[green]Sources file changed successfully![/green]") else: console.print("[red]Couldn't find the sources file![/red]") @log_start_end(log=logger) def call_height(self, other_args: List[str]): """Process height command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="height", description="select plot height (autoscaling disabled)", ) parser.add_argument( "-v", "--value", type=int, dest="value", help="value", required="-h" not in other_args, ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: self.set_and_save_preference("PLOT_PYWRY_HEIGHT", ns_parser.value) @log_start_end(log=logger) def call_width(self, other_args: List[str]): """Process width command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="width", description="select plot width (autoscaling disabled)", ) parser.add_argument( "-v", "--value", type=int, dest="value", help="value", required="-h" not in other_args, ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: self.set_and_save_preference("PLOT_PYWRY_WIDTH", ns_parser.value) @log_start_end(log=logger) def call_lang(self, other_args: List[str]): """Process lang command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="lang", description="Choose language for terminal", ) parser.add_argument( "-v", "--value", type=str, dest="value", help="Language", choices=self.languages_available, default="", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: if ns_parser.value: self.set_and_save_preference("USE_LANGUAGE", ns_parser.value) else: console.print( f"Languages available: {', '.join(self.languages_available)}" ) @log_start_end(log=logger) def call_tz(self, other_args: List[str]): """Process tz command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=""" Setting a different timezone """, ) parser.add_argument( "-t", dest="timezone", help="Choose timezone", required="-h" not in other_args, metavar="TIMEZONE", choices=pytz.all_timezones, ) if other_args and "-t" not in other_args[0]: other_args.insert(0, "-t") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser and ns_parser.timezone: self.set_and_save_preference("TIMEZONE", ns_parser.timezone) @log_start_end(log=logger) def call_flair(self, other_args: List[str]): """Process flair command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="flair", description="set the flair emoji to be used", ) parser.add_argument( "-e", "--emoji", type=str, dest="emoji", help="flair emoji to be used", nargs="+", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-e") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: if not ns_parser.emoji: ns_parser.emoji = "" else: ns_parser.emoji = " ".join(ns_parser.emoji) self.set_and_save_preference("FLAIR", ns_parser.emoji) @log_start_end(log=logger) def call_userdata(self, other_args: List[str]): """Process userdata command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="userdata", description="Set folder to store user data such as exports, presets, logs", ) parser.add_argument( "--folder", type=str, dest="folder", help="Folder where to store user data. ", default=f"{str(Path.home() / 'OpenBBUserData')}", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "--folder") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser and (other_args or self.queue): userdata_path = "" if other_args else "/" userdata_path += "/".join([ns_parser.folder] + self.queue) self.queue = [] userdata_path = userdata_path.replace("'", "").replace('"', "") default_path = Path.home() / "OpenBBUserData" success_userdata = False while not success_userdata: if userdata_path.upper() == "DEFAULT": console.print( f"User data to be saved in the default folder: '{default_path}'" ) self.set_and_save_preference("USER_DATA_DIRECTORY", default_path) success_userdata = True else: # If the path selected does not start from the user root, give relative location from root if userdata_path[0] == "~": userdata_path = userdata_path.replace( "~", os.path.expanduser("~") ) # Check if the directory exists if os.path.isdir(userdata_path): console.print( f"User data to be saved in the selected folder: '{userdata_path}'" ) self.set_and_save_preference( "USER_DATA_DIRECTORY", userdata_path ) success_userdata = True else: console.print( "[red]The path selected to user data does not exist![/red]\n" ) user_opt = "None" while user_opt not in ("Y", "N"): user_opt = input( f"Do you wish to create folder: `{userdata_path}` ? [Y/N]\n" ).upper() if user_opt == "Y": os.makedirs(userdata_path) console.print( f"[green]Folder '{userdata_path}' successfully created.[/green]" ) self.set_and_save_preference( "USER_DATA_DIRECTORY", userdata_path ) else: # Do not update userdata_folder path since we will keep the same as before console.print( "[yellow]User data to keep being saved in " + "the selected folder: " + f"{str(get_current_user().preferences.USER_DATA_DIRECTORY)}[/yellow]" ) success_userdata = True console.print()