[Feature] Generate settings menu from pydantic model (#6489)

* Generate settings menu from pydantic model

* fix tests

* fix test

* undo changes

---------

Co-authored-by: Henrique Joaquim <henriquecjoaquim@gmail.com>
This commit is contained in:
montezdesousa
2024-06-12 10:22:29 +01:00
committed by GitHub
parent aa7bccc4ec
commit 174f2893dc
5 changed files with 309 additions and 451 deletions

View File

@@ -1,15 +1,13 @@
"""Settings Controller Module."""
import argparse
from typing import List, Optional
from functools import partial, update_wrapper
from types import MethodType
from typing import List, Literal, Optional, get_origin
from openbb_cli.config.constants import AVAILABLE_FLAIRS
from openbb_cli.config.menu_text import MenuText
# pylint: disable=too-many-lines,no-member,too-many-public-methods,C0302
# pylint:disable=import-outside-toplevel
from openbb_cli.controllers.base_controller import BaseController
from openbb_cli.controllers.utils import all_timezones, is_timezone_valid
from openbb_cli.models.settings import SettingGroups
from openbb_cli.session import Session
session = Session()
@@ -18,356 +16,127 @@ session = Session()
class SettingsController(BaseController):
"""Settings Controller class."""
CHOICES_COMMANDS: List[str] = [
"interactive",
"cls",
"promptkit",
"exithelp",
"rcontext",
"richpanel",
"tbhint",
"overwrite",
"version",
"console_style",
"flair",
"timezone",
"n_rows",
"n_cols",
"obbject_msg",
"obbject_res",
"obbject_display",
]
_COMMANDS = {
v.json_schema_extra.get("command"): {
"command": (v.json_schema_extra or {}).get("command"),
"group": (v.json_schema_extra or {}).get("group"),
"description": v.description,
"annotation": v.annotation,
"field_name": k,
}
for k, v in sorted(
session.settings.model_fields.items(),
key=lambda item: (item[1].json_schema_extra or {}).get("command", ""),
)
if v.json_schema_extra
}
CHOICES_COMMANDS: List[str] = list(_COMMANDS.keys())
PATH = "/settings/"
CHOICES_GENERATION = True
def __init__(self, queue: Optional[List[str]] = None):
"""Initialize the Constructor."""
super().__init__(queue)
for cmd, field in self._COMMANDS.items():
group = field.get("group")
if group == SettingGroups.feature_flags:
self._generate_command(cmd, field, "toggle")
elif group == SettingGroups.preferences:
self._generate_command(cmd, field, "set")
self.update_completer(self.choices_default)
def print_help(self):
"""Print help."""
settings = session.settings
mt = MenuText("settings/")
mt.add_info("Feature flags")
mt.add_setting(
"interactive",
settings.USE_INTERACTIVE_DF,
description="open dataframes in interactive window",
)
mt.add_setting(
"cls",
settings.USE_CLEAR_AFTER_CMD,
description="clear console after each command",
)
mt.add_setting(
"promptkit",
settings.USE_PROMPT_TOOLKIT,
description="enable prompt toolkit (autocomplete and history)",
)
mt.add_setting(
"exithelp",
settings.ENABLE_EXIT_AUTO_HELP,
description="automatically print help when quitting menu",
)
mt.add_setting(
"rcontext",
settings.REMEMBER_CONTEXTS,
description="remember contexts between menus",
)
mt.add_setting(
"richpanel",
settings.ENABLE_RICH_PANEL,
description="colorful rich CLI panel",
)
mt.add_setting(
"tbhint",
settings.TOOLBAR_HINT,
description="displays usage hints in the bottom toolbar",
)
mt.add_setting(
"overwrite",
settings.FILE_OVERWRITE,
description="whether to overwrite Excel files if they already exists",
)
mt.add_setting(
"version",
settings.SHOW_VERSION,
description="whether to show the version in the bottom right corner",
)
mt.add_setting(
"obbject_msg",
settings.SHOW_MSG_OBBJECT_REGISTRY,
description="show obbject registry message after a new result is added",
)
mt.add_info("Feature Flags")
for k, f in self._COMMANDS.items():
if f.get("group") == SettingGroups.feature_flags:
mt.add_setting(
name=k,
status=getattr(session.settings, f["field_name"]),
description=f["description"],
)
mt.add_raw("\n")
mt.add_info("Preferences")
mt.add_cmd("console_style", description="apply a custom rich style to the CLI")
mt.add_cmd("flair", description="choose flair icon")
mt.add_cmd("timezone", description="pick timezone")
mt.add_cmd(
"n_rows", description="number of rows to show on non interactive tables"
)
mt.add_cmd(
"n_cols", description="number of columns to show on non interactive tables"
)
mt.add_cmd(
"obbject_res",
description="define the maximum number of obbjects allowed in the registry",
)
mt.add_cmd(
"obbject_display",
description="define the maximum number of cached results to display on the help menu",
)
for k, f in self._COMMANDS.items():
if f.get("group") == SettingGroups.preferences:
mt.add_cmd(
name=k,
description=f["description"],
)
session.console.print(text=mt.menu_text, menu="Settings")
def call_overwrite(self, _):
"""Process overwrite command."""
session.settings.set_item("FILE_OVERWRITE", not session.settings.FILE_OVERWRITE)
def _generate_command(
self, cmd_name: str, field: dict, action_type: Literal["toggle", "set"]
):
"""Generate command call."""
def call_version(self, _):
"""Process version command."""
session.settings.SHOW_VERSION = not session.settings.SHOW_VERSION
def call_interactive(self, _):
"""Process interactive command."""
session.settings.set_item(
"USE_INTERACTIVE_DF", not session.settings.USE_INTERACTIVE_DF
)
def call_cls(self, _):
"""Process cls command."""
session.settings.set_item(
"USE_CLEAR_AFTER_CMD", not session.settings.USE_CLEAR_AFTER_CMD
)
def call_promptkit(self, _):
"""Process promptkit command."""
session.settings.set_item(
"USE_PROMPT_TOOLKIT", not session.settings.USE_PROMPT_TOOLKIT
)
def call_exithelp(self, _):
"""Process exithelp command."""
session.settings.set_item(
"ENABLE_EXIT_AUTO_HELP", not session.settings.ENABLE_EXIT_AUTO_HELP
)
def call_rcontext(self, _):
"""Process rcontext command."""
session.settings.set_item(
"REMEMBER_CONTEXTS", not session.settings.REMEMBER_CONTEXTS
)
def call_dt(self, _):
"""Process dt command."""
session.settings.set_item("USE_DATETIME", not session.settings.USE_DATETIME)
def call_richpanel(self, _):
"""Process richpanel command."""
session.settings.set_item(
"ENABLE_RICH_PANEL", not session.settings.ENABLE_RICH_PANEL
)
def call_tbhint(self, _):
"""Process tbhint command."""
if session.settings.TOOLBAR_HINT:
session.console.print("Will take effect when running CLI again.")
session.settings.set_item("TOOLBAR_HINT", not session.settings.TOOLBAR_HINT)
def call_obbject_msg(self, _):
"""Process obbject_msg command."""
session.settings.set_item(
"SHOW_MSG_OBBJECT_REGISTRY",
not session.settings.SHOW_MSG_OBBJECT_REGISTRY,
)
def call_console_style(self, other_args: List[str]) -> None:
"""Process cosole_style command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="console_style",
description="Change your custom console style.",
add_help=False,
)
parser.add_argument(
"-s",
"--style",
dest="style",
action="store",
required=False,
choices=session.style.available_styles,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.style:
session.style.apply(ns_parser.style)
session.settings.set_item("RICH_STYLE", ns_parser.style)
elif not other_args:
session.console.print(
f"Current console style: {session.settings.RICH_STYLE}"
def _toggle(self, other_args: List[str], field=field) -> None:
"""Toggle setting value."""
field_name = field["field_name"]
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog=field["command"],
description=field["description"],
add_help=False,
)
def call_flair(self, other_args: List[str]) -> None:
"""Process flair command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="flair",
description="Change your custom flair.",
add_help=False,
)
parser.add_argument(
"-f",
"--flair",
dest="flair",
action="store",
required=False,
choices=list(AVAILABLE_FLAIRS.keys()),
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.flair:
session.settings.set_item("FLAIR", ns_parser.flair)
elif not other_args:
session.console.print(f"Current flair: {session.settings.FLAIR}")
def call_timezone(self, other_args: List[str]) -> None:
"""Process timezone command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="timezone",
description="Change your custom timezone.",
add_help=False,
)
parser.add_argument(
"-t",
"--timezone",
dest="timezone",
action="store",
required=False,
type=str,
choices=all_timezones,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.timezone:
if is_timezone_valid(ns_parser.timezone):
session.settings.set_item("TIMEZONE", ns_parser.timezone)
else:
session.console.print(
"Invalid timezone. Please enter a valid timezone."
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser:
session.settings.set_item(
field_name, not getattr(session.settings, field_name)
)
session.console.print(
f"Available timezones are: {', '.join(all_timezones)}"
)
elif not other_args:
session.console.print(f"Current timezone: {session.settings.TIMEZONE}")
def call_n_rows(self, other_args: List[str]) -> None:
"""Process n_rows command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="n_rows",
description="Number of rows to show (when not using interactive tables).",
add_help=False,
)
parser.add_argument(
"-r",
"--rows",
dest="rows",
action="store",
required=False,
type=int,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.rows:
session.settings.set_item("ALLOWED_NUMBER_OF_ROWS", ns_parser.rows)
elif not other_args:
session.console.print(
f"Current number of rows: {session.settings.ALLOWED_NUMBER_OF_ROWS}"
def _set(self, other_args: List[str], field=field) -> None:
"""Set preference value."""
field_name = field["field_name"]
annotation = field["annotation"]
command = field["command"]
type_ = str if get_origin(annotation) is Literal else annotation
choices = None
if get_origin(annotation) is Literal:
choices = annotation.__args__
elif command == "console_style":
# To have updated choices for console style
choices = session.style.available_styles
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog=command,
description=field["description"],
add_help=False,
)
def call_n_cols(self, other_args: List[str]) -> None:
"""Process n_cols command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="n_cols",
description="Number of columns to show (when not using interactive tables).",
add_help=False,
)
parser.add_argument(
"-c",
"--columns",
dest="columns",
action="store",
required=False,
type=int,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.columns:
session.settings.set_item("ALLOWED_NUMBER_OF_COLUMNS", ns_parser.columns)
elif not other_args:
session.console.print(
f"Current number of columns: {session.settings.ALLOWED_NUMBER_OF_COLUMNS}"
parser.add_argument(
"-v",
"--value",
dest="value",
action="store",
required=False,
type=type_, # type: ignore[arg-type]
choices=choices,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser:
if ns_parser.value:
# Console style is applied immediately
if command == "console_style":
session.style.apply(ns_parser.value)
session.settings.set_item(field_name, ns_parser.value)
session.console.print(
f"[info]Current value:[/info] {getattr(session.settings, field_name)}"
)
elif not other_args:
session.console.print(
f"[info]Current value:[/info] {getattr(session.settings, field_name)}"
)
def call_obbject_res(self, other_args: List[str]):
"""Process obbject_res command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="obbject_res",
description="Maximum allowed number of results to keep in the OBBject Registry.",
add_help=False,
action = None
if action_type == "toggle":
action = _toggle
elif action_type == "set":
action = _set
else:
raise ValueError(f"Action type '{action_type}' not allowed.")
bound_method = update_wrapper(
wrapper=partial(MethodType(action, self), field=field), wrapped=action
)
parser.add_argument(
"-n",
"--number",
dest="number",
action="store",
required=False,
type=int,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.number:
session.settings.set_item("N_TO_KEEP_OBBJECT_REGISTRY", ns_parser.number)
elif not other_args:
session.console.print(
f"Current maximum allowed number of results to keep in the OBBject registry:"
f" {session.settings.N_TO_KEEP_OBBJECT_REGISTRY}"
)
def call_obbject_display(self, other_args: List[str]):
"""Process obbject_display command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="obbject_display",
description="Number of results to display from the OBBject Registry.",
add_help=False,
)
parser.add_argument(
"-n",
"--number",
dest="number",
action="store",
required=False,
type=int,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.number:
session.settings.set_item("N_TO_DISPLAY_OBBJECT_REGISTRY", ns_parser.number)
elif not other_args:
session.console.print(
f"Current number of results to display from the OBBject registry:"
f" {session.settings.N_TO_DISPLAY_OBBJECT_REGISTRY}"
)
setattr(self, f"call_{cmd_name}", bound_method)

View File

@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
import numpy as np
import pandas as pd
import requests
from openbb import obb
from openbb_charting.core.backend import create_backend, get_backend
from openbb_cli.config.constants import AVAILABLE_FLAIRS, ENV_FILE_SETTINGS
from openbb_cli.session import Session
@@ -23,8 +24,6 @@ from openbb_core.app.model.charts.charting_settings import ChartingSettings
from pytz import all_timezones, timezone
from rich.table import Table
from openbb import obb
if TYPE_CHECKING:
from openbb_charting.core.openbb_figure import OpenBBFigure
@@ -33,6 +32,8 @@ if TYPE_CHECKING:
# pylint: disable=too-many-statements,no-member,too-many-branches,C0302
session = Session()
def remove_file(path: Path) -> bool:
"""Remove path.
@@ -55,7 +56,7 @@ def remove_file(path: Path) -> bool:
shutil.rmtree(path)
return True
except Exception:
Session().console.print(
session.console.print(
f"\n[bold red]Failed to remove {path}"
"\nPlease delete this manually![/bold red]"
)
@@ -88,17 +89,17 @@ Please feel free to check out our other products:
[bold]OpenBB Platform:[/] [cmds]https://openbb.co/products/platform[/cmds]
[bold]OpenBB Bot[/]: [cmds]https://openbb.co/products/bot[/cmds]
"""
Session().console.print(text)
session.console.print(text)
def print_guest_block_msg():
"""Block guest users from using the cli."""
if Session().is_local():
Session().console.print(
if session.is_local():
session.console.print(
"[info]You are currently logged as a guest.[/info]\n"
"[info]Login to use this feature.[/info]\n\n"
"[info]If you don't have an account, you can create one here: [/info]"
f"[cmds]{Session().settings.HUB_URL + '/register'}\n[/cmds]"
f"[cmds]{session.settings.HUB_URL + '/register'}\n[/cmds]"
)
@@ -115,7 +116,7 @@ def bootup():
# pylint: disable=E1101
sys.stdout.reconfigure(encoding="utf-8") # type: ignore
except Exception as e:
Session().console.print(e, "\n")
session.console.print(e, "\n")
def welcome_message():
@@ -123,8 +124,8 @@ def welcome_message():
Prints first welcome message, help and a notification if updates are available.
"""
Session().console.print(
f"\nWelcome to OpenBB Platform CLI v{Session().settings.VERSION}"
session.console.print(
f"\nWelcome to OpenBB Platform CLI v{session.settings.VERSION}"
)
@@ -133,16 +134,15 @@ def reset(queue: Optional[List[str]] = None):
Allows for checking code without quitting.
"""
Session().console.print("resetting...")
Session().reset()
debug = Session().settings.DEBUG_MODE
dev = Session().settings.DEV_BACKEND
session.console.print("resetting...")
debug = session.settings.DEBUG_MODE
dev = session.settings.DEV_BACKEND
try:
# remove the hub routines
if not Session().is_local():
if not session.is_local():
remove_file(
Path(Session().user.preferences.export_directory, "routines", "hub")
Path(session.user.preferences.export_directory, "routines", "hub")
)
# if not get_current_user().profile.remember:
@@ -157,7 +157,7 @@ def reset(queue: Optional[List[str]] = None):
queue_list = ["/".join(queue) if len(queue) > 0 else ""] # type: ignore
# pylint: disable=import-outside-toplevel
# we run the cli again
if Session().is_local():
if session.is_local():
from openbb_cli.controllers.cli_controller import main
main(debug, dev, queue_list, module="") # type: ignore
@@ -167,7 +167,7 @@ def reset(queue: Optional[List[str]] = None):
launch(queue=queue_list)
except Exception as e:
Session().console.print(f"Unfortunately, resetting wasn't possible: {e}\n")
session.console.print(f"Unfortunately, resetting wasn't possible: {e}\n")
print_goodbye()
@@ -198,7 +198,7 @@ def first_time_user() -> bool:
Whether or not the user is a first time user
"""
if ENV_FILE_SETTINGS.stat().st_size == 0:
Session().settings.set_item("PREVIOUS_USE", True)
session.settings.set_item("PREVIOUS_USE", True)
return True
return False
@@ -258,7 +258,7 @@ def parse_and_split_input(an_input: str, custom_filters: List) -> List[str]:
else:
filter_input = False
commands = an_input.split("/") if "-t" not in an_input else [an_input]
commands = an_input.split("/") if "timezone" not in an_input else [an_input]
for command_num, command in enumerate(commands):
if command == commands[command_num] == commands[-1] == "":
@@ -367,8 +367,8 @@ def print_rich_table( # noqa: PLR0912
if export:
return
MAX_COLS = Session().settings.ALLOWED_NUMBER_OF_COLUMNS
MAX_ROWS = Session().settings.ALLOWED_NUMBER_OF_ROWS
MAX_COLS = session.settings.ALLOWED_NUMBER_OF_COLUMNS
MAX_ROWS = session.settings.ALLOWED_NUMBER_OF_ROWS
# Make a copy of the dataframe to avoid SettingWithCopyWarning
df = df.copy()
@@ -397,7 +397,7 @@ def print_rich_table( # noqa: PLR0912
raise ValueError("Length of headers does not match length of DataFrame.")
return output
if Session().settings.USE_INTERACTIVE_DF:
if session.settings.USE_INTERACTIVE_DF:
df_outgoing = df.copy()
# If headers are provided, use them
if headers is not None:
@@ -419,7 +419,7 @@ def print_rich_table( # noqa: PLR0912
_get_backend().send_table(
df_table=df_outgoing,
title=title,
theme=Session().user.preferences.table_style,
theme=session.user.preferences.table_style,
)
return
@@ -501,24 +501,24 @@ def print_rich_table( # noqa: PLR0912
for idx, x in enumerate(values)
]
table.add_row(*row_idx)
Session().console.print(table)
session.console.print(table)
else:
Session().console.print(df.to_string(col_space=0))
session.console.print(df.to_string(col_space=0))
if exceeds_allowed_columns:
Session().console.print(
f"[yellow]\nAllowed number of columns exceeded ({Session().settings.ALLOWED_NUMBER_OF_COLUMNS}).\n"
session.console.print(
f"[yellow]\nAllowed number of columns exceeded ({session.settings.ALLOWED_NUMBER_OF_COLUMNS}).\n"
f"The following columns were removed from the output: {', '.join(trimmed_columns)}.\n[/yellow]"
)
if exceeds_allowed_rows:
Session().console.print(
f"[yellow]\nAllowed number of rows exceeded ({Session().settings.ALLOWED_NUMBER_OF_ROWS}).\n"
session.console.print(
f"[yellow]\nAllowed number of rows exceeded ({session.settings.ALLOWED_NUMBER_OF_ROWS}).\n"
f"{trimmed_rows_count} rows were removed from the output.\n[/yellow]"
)
if exceeds_allowed_columns or exceeds_allowed_rows:
Session().console.print(
session.console.print(
"Use the `--export` flag to analyse the full output on a file."
)
@@ -556,7 +556,7 @@ def get_user_agent() -> str:
def get_flair() -> str:
"""Get a flair icon."""
current_flair = str(Session().settings.FLAIR)
current_flair = str(session.settings.FLAIR)
flair = AVAILABLE_FLAIRS.get(current_flair, current_flair)
return flair
@@ -564,7 +564,7 @@ def get_flair() -> str:
def get_dtime() -> str:
"""Get a datetime string."""
dtime = ""
if Session().settings.USE_DATETIME and get_user_timezone_or_invalid() != "INVALID":
if session.settings.USE_DATETIME and get_user_timezone_or_invalid() != "INVALID":
dtime = datetime.now(timezone(get_user_timezone())).strftime("%Y %b %d, %H:%M")
return dtime
@@ -572,12 +572,10 @@ def get_dtime() -> str:
def get_flair_and_username() -> str:
"""Get a flair icon and username."""
flair = get_flair()
dtime = get_dtime()
if dtime:
if dtime := get_dtime():
dtime = f"{dtime} "
username = getattr(Session().user.profile.hub_session, "username", "")
username = getattr(session.user.profile.hub_session, "username", "")
if username:
username = f"[{username}] "
@@ -608,7 +606,7 @@ def get_user_timezone() -> str:
str
user timezone based on .env file
"""
return Session().settings.TIMEZONE
return session.settings.TIMEZONE
def get_user_timezone_or_invalid() -> str:
@@ -659,7 +657,7 @@ def check_file_type_saved(valid_types: Optional[List[str]] = None):
if filename.endswith(tuple(valid_types)):
valid_filenames.append(filename)
else:
Session().console.print(
session.console.print(
f"[red]Filename '{filename}' provided is not valid!\nPlease use one of the following file types:"
f"{','.join(valid_types)}[/red]\n"
)
@@ -737,7 +735,7 @@ def compose_export_path(func_name: str, dir_path: str) -> Path:
default_filename = f"{now.strftime('%Y%m%d_%H%M%S')}_{path_cmd}_{func_name}"
full_path = Path(Session().user.preferences.export_directory) / default_filename
full_path = Path(session.user.preferences.export_directory) / default_filename
return full_path
@@ -748,9 +746,9 @@ def ask_file_overwrite(file_path: Path) -> Tuple[bool, bool]:
Returns two values, the first is a boolean indicating if the file exists and the
second is a boolean indicating if the user wants to overwrite the file.
"""
if Session().settings.FILE_OVERWRITE:
if session.settings.FILE_OVERWRITE:
return False, True
if Session().settings.TEST_MODE:
if session.settings.TEST_MODE:
return False, True
if file_path.exists():
overwrite = input("\nFile already exists. Overwrite? [y/n]: ").lower()
@@ -902,17 +900,17 @@ def export_data(
elif saved_path.suffix in [".jpg", ".png"]:
if figure is None:
Session().console.print("No plot to export.")
session.console.print("No plot to export.")
continue
figure.show(export_image=saved_path, margin=margin)
else:
Session().console.print("Wrong export file specified.")
session.console.print("Wrong export file specified.")
continue
if saved_path.exists():
Session().console.print(f"Saved file: {saved_path}")
session.console.print(f"Saved file: {saved_path}")
else:
Session().console.print(f"Failed to save file: {saved_path}")
session.console.print(f"Failed to save file: {saved_path}")
if figure is not None:
figure._exported = True # pylint: disable=protected-access
@@ -956,7 +954,7 @@ def request(
# If there are headers, check if there is a user agent, if not add one.
# Some requests seem to work only with a specific user agent, so we want to be able to override it.
headers = kwargs.pop("headers", {})
timeout = timeout or Session().user.preferences.request_timeout
timeout = timeout or session.user.preferences.request_timeout
if "User-Agent" not in headers:
headers["User-Agent"] = get_user_agent()

View File

@@ -1,15 +1,24 @@
"""Settings model."""
from typing import Any
from enum import Enum
from typing import Any, Literal
from dotenv import dotenv_values, set_key
from openbb_cli.config.constants import ENV_FILE_SETTINGS
from openbb_cli.config.constants import AVAILABLE_FLAIRS, ENV_FILE_SETTINGS
from openbb_core.app.version import get_package_version
from pydantic import BaseModel, ConfigDict, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pytz import all_timezones
VERSION = get_package_version("openbb-cli")
class SettingGroups(Enum):
"""Setting types."""
feature_flags = "feature_flag"
preferences = "preference"
class Settings(BaseModel):
"""Settings model."""
@@ -21,37 +30,125 @@ class Settings(BaseModel):
DEBUG_MODE: bool = False
DEV_BACKEND: bool = False
# FEATURE FLAGS
FILE_OVERWRITE: bool = False
SHOW_VERSION: bool = True
USE_INTERACTIVE_DF: bool = True
USE_CLEAR_AFTER_CMD: bool = False
USE_DATETIME: bool = True
USE_PROMPT_TOOLKIT: bool = True
ENABLE_EXIT_AUTO_HELP: bool = True
REMEMBER_CONTEXTS: bool = True
ENABLE_RICH_PANEL: bool = True
TOOLBAR_HINT: bool = True
SHOW_MSG_OBBJECT_REGISTRY: bool = False
# GENERAL
TIMEZONE: str = "America/New_York"
FLAIR: str = ":openbb"
PREVIOUS_USE: bool = False
N_TO_KEEP_OBBJECT_REGISTRY: int = 10
N_TO_DISPLAY_OBBJECT_REGISTRY: int = 5
# STYLE
RICH_STYLE: str = "dark"
# OUTPUT
ALLOWED_NUMBER_OF_ROWS: int = 20
ALLOWED_NUMBER_OF_COLUMNS: int = 5
# OPENBB
HUB_URL: str = "https://my.openbb.co"
BASE_URL: str = "https://payments.openbb.co"
# GENERAL
PREVIOUS_USE: bool = False
# FEATURE FLAGS
FILE_OVERWRITE: bool = Field(
default=False,
description="whether to overwrite Excel files if they already exists",
command="overwrite",
group=SettingGroups.feature_flags,
)
SHOW_VERSION: bool = Field(
default=True,
description="whether to show the version in the bottom right corner",
command="version",
group=SettingGroups.feature_flags,
)
USE_INTERACTIVE_DF: bool = Field(
default=True,
description="display tables in interactive window",
command="interactive",
group=SettingGroups.feature_flags,
)
USE_CLEAR_AFTER_CMD: bool = Field(
default=False,
description="clear console after each command",
command="cls",
group=SettingGroups.feature_flags,
)
USE_DATETIME: bool = Field(
default=True,
description="whether to show the date and time before the flair",
command="datetime",
group=SettingGroups.feature_flags,
)
USE_PROMPT_TOOLKIT: bool = Field(
default=True,
description="enable prompt toolkit (autocomplete and history)",
command="promptkit",
group=SettingGroups.feature_flags,
)
ENABLE_EXIT_AUTO_HELP: bool = Field(
default=True,
description="automatically print help when quitting menu",
command="exithelp",
group=SettingGroups.feature_flags,
)
REMEMBER_CONTEXTS: bool = Field(
default=True,
description="remember contexts between menus",
command="rcontext",
group=SettingGroups.feature_flags,
)
ENABLE_RICH_PANEL: bool = Field(
default=True,
description="enable colorful rich CLI panel",
command="richpanel",
group=SettingGroups.feature_flags,
)
TOOLBAR_HINT: bool = Field(
default=True,
description="displays usage hints in the bottom toolbar",
command="tbhint",
group=SettingGroups.feature_flags,
)
SHOW_MSG_OBBJECT_REGISTRY: bool = Field(
default=False,
description="show obbject registry message after a new result is added",
command="obbject_msg",
group=SettingGroups.feature_flags,
)
# PREFERENCES
TIMEZONE: Literal[tuple(all_timezones)] = Field( # type: ignore[valid-type]
default="America/New_York",
description="pick timezone",
command="timezone",
group=SettingGroups.preferences,
)
FLAIR: Literal[tuple(AVAILABLE_FLAIRS)] = Field( # type: ignore[valid-type]
default=":openbb",
description="choose flair icon",
command="flair",
group=SettingGroups.preferences,
)
N_TO_KEEP_OBBJECT_REGISTRY: int = Field(
default=10,
description="define the maximum number of obbjects allowed in the registry",
command="obbject_res",
group=SettingGroups.preferences,
)
N_TO_DISPLAY_OBBJECT_REGISTRY: int = Field(
default=5,
description="define the maximum number of cached results to display on the help menu",
command="obbject_display",
group=SettingGroups.preferences,
)
RICH_STYLE: str = Field(
default="dark",
description="apply a custom rich style to the CLI",
command="console_style",
group=SettingGroups.preferences,
)
ALLOWED_NUMBER_OF_ROWS: int = Field(
default=20,
description="number of rows to show (when not using interactive tables).",
command="n_rows",
group=SettingGroups.preferences,
)
ALLOWED_NUMBER_OF_COLUMNS: int = Field(
default=5,
description="number of columns to show (when not using interactive tables).",
command="n_cols",
group=SettingGroups.preferences,
)
model_config = ConfigDict(validate_assignment=True)
def __repr__(self) -> str:

View File

@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
import pytest
from openbb_cli.controllers.settings_controller import SettingsController
# pylint: disable=redefined-outer-name, unused-argument
# pylint: disable=redefined-outer-name, unused-argument, no-member
@pytest.fixture
@@ -14,7 +14,7 @@ def mock_session():
mock.settings.USE_INTERACTIVE_DF = False
mock.settings.ALLOWED_NUMBER_OF_ROWS = 20
mock.settings.TIMEZONE = "UTC"
mock.style.available_styles = ["dark"]
mock.settings.set_item = MagicMock()
yield mock
@@ -35,7 +35,7 @@ def test_call_interactive(mock_session):
)
def test_call_n_rows(input_rows, expected, mock_session):
controller = SettingsController()
args = ["--rows", str(input_rows)]
args = ["--value", str(input_rows)]
controller.call_n_rows(args)
mock_session.settings.set_item.assert_called_with(
"ALLOWED_NUMBER_OF_ROWS", expected
@@ -45,7 +45,7 @@ def test_call_n_rows(input_rows, expected, mock_session):
def test_call_n_rows_no_args_provided(mock_session):
controller = SettingsController()
controller.call_n_rows([])
mock_session.console.print.assert_called_with("Current number of rows: 20")
mock_session.console.print.assert_called_with("[info]Current value:[/info] 20")
@pytest.mark.parametrize(
@@ -56,22 +56,18 @@ def test_call_n_rows_no_args_provided(mock_session):
],
)
def test_call_timezone(timezone, valid, mock_session):
with patch(
"openbb_cli.controllers.settings_controller.is_timezone_valid",
return_value=valid,
):
controller = SettingsController()
args = ["--timezone", timezone]
controller.call_timezone(args)
if valid:
mock_session.settings.set_item.assert_called_with("TIMEZONE", timezone)
else:
mock_session.settings.set_item.assert_not_called()
controller = SettingsController()
args = ["--value", timezone]
controller.call_timezone(args)
if valid:
mock_session.settings.set_item.assert_called_with("TIMEZONE", timezone)
else:
mock_session.settings.set_item.assert_not_called()
def test_call_console_style(mock_session):
controller = SettingsController()
args = ["--style", "dark"]
args = ["--value", "dark"]
controller.call_console_style(args)
mock_session.console.print.assert_called()
@@ -80,25 +76,25 @@ def test_call_console_style_no_args(mock_session):
mock_session.settings.RICH_STYLE = "default"
controller = SettingsController()
controller.call_console_style([])
mock_session.console.print.assert_called_with("Current console style: default")
mock_session.console.print.assert_called_with("[info]Current value:[/info] default")
def test_call_flair(mock_session):
controller = SettingsController()
args = ["--flair", "rocket"]
args = ["--value", "rocket"]
controller.call_flair(args)
def test_call_flair_no_args(mock_session):
mock_session.settings.FLAIR = "star"
mock_session.settings.FLAIR = "bug"
controller = SettingsController()
controller.call_flair([])
mock_session.console.print.assert_called_with("Current flair: star")
mock_session.console.print.assert_called_with("[info]Current value:[/info] bug")
def test_call_obbject_display(mock_session):
controller = SettingsController()
args = ["--number", "5"]
args = ["--value", "5"]
controller.call_obbject_display(args)
mock_session.settings.set_item.assert_called_once_with(
"N_TO_DISPLAY_OBBJECT_REGISTRY", 5
@@ -109,16 +105,14 @@ def test_call_obbject_display_no_args(mock_session):
mock_session.settings.N_TO_DISPLAY_OBBJECT_REGISTRY = 10
controller = SettingsController()
controller.call_obbject_display([])
mock_session.console.print.assert_called_with(
"Current number of results to display from the OBBject registry: 10"
)
mock_session.console.print.assert_called_with("[info]Current value:[/info] 10")
@pytest.mark.parametrize(
"args, expected",
[
(["--rows", "50"], 50),
(["--rows", "100"], 100),
(["--value", "50"], 50),
(["--value", "100"], 100),
([], 20),
],
)
@@ -131,4 +125,4 @@ def test_call_n_rows_v2(args, expected, mock_session):
"ALLOWED_NUMBER_OF_ROWS", expected
)
else:
mock_session.console.print.assert_called_with("Current number of rows: 20")
mock_session.console.print.assert_called_with("[info]Current value:[/info] 20")

View File

@@ -23,13 +23,13 @@ from openbb_cli.controllers.utils import (
@pytest.fixture
def mock_session():
"""Mock the session and its dependencies."""
with patch("openbb_cli.controllers.utils.Session", autospec=True) as mock:
mock.return_value.console.print = MagicMock()
mock.return_value.is_local = MagicMock(return_value=True)
mock.return_value.settings.VERSION = "1.0"
mock.return_value.user.profile.hub_session.username = "testuser"
mock.return_value.settings.FLAIR = "rocket"
yield mock
with patch("openbb_cli.controllers.utils.session") as mock_session:
mock_session.console.print = MagicMock()
mock_session.is_local = MagicMock(return_value=True)
mock_session.settings.VERSION = "1.0"
mock_session.user.profile.hub_session.username = "testuser"
mock_session.settings.FLAIR = "rocket"
yield mock_session
def test_remove_file_existing_file():
@@ -54,13 +54,13 @@ def test_remove_file_failure(mock_session):
"os.remove", side_effect=Exception("Error")
):
assert not remove_file(Path("/path/to/file"))
mock_session.return_value.console.print.assert_called()
mock_session.console.print.assert_called()
def test_print_goodbye(mock_session):
"""Test printing the goodbye message."""
print_goodbye()
mock_session.return_value.console.print.assert_called()
mock_session.console.print.assert_called()
def test_parse_and_split_input():
@@ -87,13 +87,13 @@ def test_parse_and_split_input_special_cases(input_command, expected_output):
def test_print_guest_block_msg(mock_session):
"""Test printing the guest block message."""
print_guest_block_msg()
mock_session.return_value.console.print.assert_called()
mock_session.console.print.assert_called()
def test_welcome_message(mock_session):
"""Test printing the welcome message."""
welcome_message()
mock_session.return_value.console.print.assert_called_with(
mock_session.console.print.assert_called_with(
"\nWelcome to OpenBB Platform CLI v1.0"
)