From 7d3172a5f5930f2d1cd7b0b18d1b8eaa6825a9b4 Mon Sep 17 00:00:00 2001 From: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Date: Thu, 13 Apr 2023 13:44:31 +0100 Subject: [PATCH] Make default routines available & download personal routines on login (#4737) * make default routines available * remove unecessary change * user to personal * rename some funcs * add last update to tables * add routines download on login * move some routines funcs to controller * fix tests * add space * wrap some funcs inside try, except * fix account test * suppress outputs * rewrite docstring * remove circular import * reverse order * update docstring * allow exe with spaces in file name * fix exe with input * Update terminal_controller.py * fix regex for file extensions * pylint * put default routines url inside constants --------- Co-authored-by: Jeroen Bouma Co-authored-by: James Maslek --- openbb_terminal/account/__init__.py | 0 openbb_terminal/account/account_controller.py | 180 ++++++++++++------ openbb_terminal/account/account_view.py | 56 ++++-- openbb_terminal/core/session/__init__.py | 0 openbb_terminal/core/session/constants.py | 3 +- openbb_terminal/core/session/hub_model.py | 67 ++++++- openbb_terminal/core/session/local_model.py | 13 +- .../session/routines_handler.py} | 137 ++++++++----- .../core/session/session_controller.py | 1 + openbb_terminal/core/session/session_model.py | 72 +++++-- openbb_terminal/core/session/utils.py | 14 +- openbb_terminal/terminal_controller.py | 76 +++----- .../account/test_account_controller.py | 35 +++- .../test_call_list.txt | 10 +- .../test_call_sync[other_args0-False].txt | 1 - .../test_call_sync[other_args1-True].txt | 1 - .../test_call_sync[other_args2-True].txt | 1 - .../test_call_sync[other_args3-False].txt | 1 - .../test_call_sync[other_args4-True].txt | 1 - .../openbb_terminal/session/test_hub_model.py | 2 +- .../test_routines_handler.py} | 10 +- 21 files changed, 467 insertions(+), 214 deletions(-) create mode 100644 openbb_terminal/account/__init__.py create mode 100644 openbb_terminal/core/session/__init__.py rename openbb_terminal/{account/account_model.py => core/session/routines_handler.py} (55%) delete mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt delete mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt delete mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt delete mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt delete mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt rename tests/openbb_terminal/{account/test_account_model.py => session/test_routines_handler.py} (92%) diff --git a/openbb_terminal/account/__init__.py b/openbb_terminal/account/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index c2e7940cbda9..c5848dc6b8b8 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -3,18 +3,21 @@ from pathlib import Path from typing import Dict, List, Optional -from openbb_terminal.account.account_model import ( - get_routines_info, - read_routine, - save_routine, - set_login_called, +from openbb_terminal.account.account_view import ( + display_default_routines, + display_personal_routines, ) -from openbb_terminal.account.account_view import display_routines_list from openbb_terminal.core.session import hub_model as Hub from openbb_terminal.core.session.current_user import ( get_current_user, is_local, ) +from openbb_terminal.core.session.routines_handler import ( + get_default_routines_info, + get_personal_routines_info, + read_routine, + save_routine, +) from openbb_terminal.core.session.session_model import logout from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end @@ -27,6 +30,32 @@ logger = logging.getLogger(__name__) +__login_called = False + + +def get_login_called(): + """Get the login/logout called flag. + + Returns + ------- + bool + The login/logout called flag. + """ + return __login_called + + +def set_login_called(value: bool): + """Set the login/logout called flag. + + Parameters + ---------- + value : bool + The login/logout called flag. + """ + global __login_called # pylint: disable=global-statement + __login_called = value + + class AccountController(BaseController): """Account Controller Class""" @@ -49,27 +78,41 @@ class AccountController(BaseController): def __init__(self, queue: Optional[List[str]] = None): """Constructor""" super().__init__(queue) - self.ROUTINE_FILES: Dict[str, Path] = {} + self.LOCAL_ROUTINES: Dict[str, Path] = {} self.REMOTE_CHOICES: List[str] = [] + + self.DEFAULT_ROUTINES: List[Dict[str, str]] = self.fetch_default_routines() + self.DEFAULT_CHOICES: List[str] = [ + r["name"] for r in self.DEFAULT_ROUTINES if "name" in r + ] + if session and get_current_user().preferences.USE_PROMPT_TOOLKIT: self.choices: dict = self.choices_default self.completer = NestedCompleter.from_nested_dict(self.choices) def update_runtime_choices(self): """Update runtime choices""" - self.ROUTINE_FILES = self.get_routines() + self.LOCAL_ROUTINES = self.get_local_routines() if session and get_current_user().preferences.USE_PROMPT_TOOLKIT: - self.choices["upload"]["--file"].update({c: {} for c in self.ROUTINE_FILES}) + self.choices["upload"]["--file"].update( + {c: {} for c in self.LOCAL_ROUTINES} + ) self.choices["download"]["--name"].update( - {c: {} for c in self.REMOTE_CHOICES} + {c: {} for c in self.DEFAULT_CHOICES + self.REMOTE_CHOICES} ) self.choices["delete"]["--name"].update( {c: {} for c in self.REMOTE_CHOICES} ) self.completer = NestedCompleter.from_nested_dict(self.choices) - def get_routines(self): - """Get routines""" + def get_local_routines(self) -> Dict[str, Path]: + """Get local routines + + Returns + ------- + Dict[str, Path] + The local routines + """ current_user = get_current_user() return { filepath.name: filepath @@ -78,6 +121,20 @@ def get_routines(self): ) } + def fetch_default_routines(self) -> List[Dict[str, str]]: + """Fetch default routines + + Returns + ------- + List[Dict[str, str]] + The default routines + """ + response = Hub.get_default_routines() + if response and response.status_code == 200: + d = response.json() + return d.get("data", []) + return [] + def print_help(self): """Print help""" mt = MenuText("account/", 100) @@ -201,13 +258,16 @@ def call_list(self, other_args: List[str]): page=ns_parser.page, size=ns_parser.size, ) - df, page, pages = get_routines_info(response) + df, page, pages = get_personal_routines_info(response) if not df.empty: self.REMOTE_CHOICES += list(df["name"]) self.update_runtime_choices() - display_routines_list(df, page, pages) + display_personal_routines(df, page, pages) else: console.print("[red]No routines found.[/red]") + console.print("") + df = get_default_routines_info(self.DEFAULT_ROUTINES) + display_default_routines(df) @log_start_end(log=logger) def call_upload(self, other_args: List[str]): @@ -320,49 +380,61 @@ def call_download(self, other_args: List[str]): print_guest_block_msg() else: if ns_parser: - response = Hub.download_routine( - auth_header=get_current_user().profile.get_auth_header(), - name=" ".join(ns_parser.name), - ) + data = None - if response and response.status_code == 200: - data = response.json() - if data: - name = data.get("name", "") - if name: - console.print(f"[info]Name:[/info] {name}") - - description = data.get("description", "") - if description: - console.print(f"[info]Description:[/info] {description}") - - script = data.get("script", "") - if script: - file_name = f"{name}.openbb" - file_path = save_routine( - file_name=file_name, - routine=script, + # Default routine + name = " ".join(ns_parser.name) + if name in self.DEFAULT_CHOICES: + data = next( + (r for r in self.DEFAULT_ROUTINES if r["name"] == name), None + ) + else: + # User routine + response = Hub.download_routine( + auth_header=get_current_user().profile.get_auth_header(), + name=name, + ) + data = ( + response.json() + if response and response.status_code == 200 + else None + ) + + # Save routine + if data: + name = data.get("name", "") + if name: + console.print(f"[info]Name:[/info] {name}") + + description = data.get("description", "") + if description: + console.print(f"[info]Description:[/info] {description}") + + script = data.get("script", "") + if script: + file_name = f"{name}.openbb" + file_path = save_routine( + file_name=file_name, + routine=script, + ) + if file_path == "File already exists": + i = console.input( + "\nA file with the same name already exists, " + "do you want to replace it? (y/n): " ) - if file_path == "File already exists": - i = console.input( - "\nA file with the same name already exists, " - "do you want to replace it? (y/n): " + console.print("") + if i.lower() in ["y", "yes"]: + file_path = save_routine( + file_name=file_name, + routine=script, + force=True, ) - console.print("") - if i.lower() in ["y", "yes"]: - file_path = save_routine( - file_name=file_name, - routine=script, - force=True, - ) - if file_path: - console.print( - f"[info]Location:[/info] {file_path}" - ) - else: - console.print("[info]Aborted.[/info]") - elif file_path: - console.print(f"[info]Location:[/info] {file_path}") + if file_path: + console.print(f"[info]Location:[/info] {file_path}") + else: + console.print("[info]Aborted.[/info]") + elif file_path: + console.print(f"[info]Location:[/info] {file_path}") @log_start_end(log=logger) def call_delete(self, other_args: List[str]): diff --git a/openbb_terminal/account/account_view.py b/openbb_terminal/account/account_view.py index f9c7bcd1c261..45f2f93a4e2f 100644 --- a/openbb_terminal/account/account_view.py +++ b/openbb_terminal/account/account_view.py @@ -1,10 +1,11 @@ import pandas as pd from openbb_terminal.helper_funcs import print_rich_table +from openbb_terminal.rich_config import console -def display_routines_list(df: pd.DataFrame, page: int, pages: int): - """Display the routines list. +def display_personal_routines(df: pd.DataFrame, page: int, pages: int): + """Display the routines. Parameters ---------- @@ -15,14 +16,43 @@ def display_routines_list(df: pd.DataFrame, page: int, pages: int): pages : int The total number of pages. """ - title = f"Available routines - page {page}" - if pages: - title += f" of {pages}" - - print_rich_table( - df=df, - title=title, - headers=["Name", "Description"], - show_index=True, - index_name="#", - ) + try: + title = f"Personal routines - page {page}" + if pages: + title += f" of {pages}" + + df["updated_date"] = pd.to_datetime(df["updated_date"]) + df["updated_date"] = df["updated_date"].dt.strftime("%Y-%m-%d %H:%M:%S") + df.replace(to_replace=[None], value="-", inplace=True) + print_rich_table( + df=df, + title=title, + headers=["Name", "Description", "Version", "Last update"], + show_index=True, + index_name="#", + ) + except Exception: + console.print("Failed to display personal routines.") + + +def display_default_routines(df: pd.DataFrame): + """Display the default routines. + + Parameters + ---------- + df : pd.DataFrame + The default routines list. + """ + try: + df["date_updated"] = pd.to_datetime(df["date_updated"]) + df["date_updated"] = df["date_updated"].dt.strftime("%Y-%m-%d %H:%M:%S") + df.replace(to_replace=[None], value="-", inplace=True) + print_rich_table( + df=df, + title="Default routines", + headers=["Name", "Description", "Version", "Last update"], + show_index=True, + index_name="#", + ) + except Exception: + console.print("Failed to display default routines.") diff --git a/openbb_terminal/core/session/__init__.py b/openbb_terminal/core/session/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openbb_terminal/core/session/constants.py b/openbb_terminal/core/session/constants.py index b928b52cb3bd..a4c05cf0578a 100644 --- a/openbb_terminal/core/session/constants.py +++ b/openbb_terminal/core/session/constants.py @@ -9,7 +9,8 @@ COLORS_URL = HUB_URL + "app/terminal/theme" CHARTS_TABLES_URL = HUB_URL + "app/terminal/theme/charts-tables" -TIMEOUT = 15 +DEFAULT_ROUTINES_URL = "https://tffo1zc1.directus.app/items/Routines" +TIMEOUT = 15 CONNECTION_ERROR_MSG = "[red]Connection error.[/red]" CONNECTION_TIMEOUT_MSG = "[red]Connection timeout.[/red]" diff --git a/openbb_terminal/core/session/hub_model.py b/openbb_terminal/core/session/hub_model.py index 8bf1be91ea10..bbadd9a9d0a6 100644 --- a/openbb_terminal/core/session/hub_model.py +++ b/openbb_terminal/core/session/hub_model.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, Literal, Optional +from typing import Any, Dict, List, Literal, Optional import requests @@ -7,6 +7,7 @@ BASE_URL, CONNECTION_ERROR_MSG, CONNECTION_TIMEOUT_MSG, + DEFAULT_ROUTINES_URL, TIMEOUT, ) from openbb_terminal.core.session.current_system import get_current_system @@ -301,15 +302,15 @@ def upload_user_field( timeout : int The timeout, by default TIMEOUT silent : bool - Whether to print the status, by default False + Whether to silence the console output, by default False Returns ------- Optional[requests.Response] The response from the put request. """ + console_print = console.print if not silent else lambda *args, **kwargs: None try: - console_print = console.print if not silent else lambda *args, **kwargs: None data: Dict[str, dict] = {key: value} console_print("Sending to OpenBB hub...") @@ -556,10 +557,12 @@ def delete_routine( def list_routines( auth_header: str, + fields: Optional[List[str]] = None, page: int = 1, size: int = 10, base_url=BASE_URL, timeout: int = TIMEOUT, + silent: bool = False, ) -> Optional[requests.Response]: """List all routines from the server. @@ -567,6 +570,8 @@ def list_routines( ---------- auth_header : str The authorization header, e.g. "Bearer ". + fields : Optional[List[str]] + The fields to return, by default None page : int The page number. size : int @@ -575,31 +580,73 @@ def list_routines( The base url, by default BASE_URL timeout : int The timeout, by default TIMEOUT + silent : bool + Whether to silence the console output, by default False Returns ------- Optional[requests.Response] The response from the get request. """ - fields = "name%2Cdescription" - + console_print = console.print if not silent else lambda *args, **kwargs: None try: + if fields is None: + fields = ["name", "description", "version", "updated_date"] + + fields_str = "%2C".join(fields) response = requests.get( headers={"Authorization": auth_header}, - url=f"{base_url}terminal/script?fields={fields}&page={page}&size={size}", + url=f"{base_url}terminal/script?fields={fields_str}&page={page}&size={size}", timeout=timeout, ) if response.status_code != 200: - console.print("[red]Failed to list your routines.[/red]") + console_print("[red]Failed to list your routines.[/red]") return response except requests.exceptions.ConnectionError: - console.print(f"\n{CONNECTION_ERROR_MSG}") + console_print(f"\n{CONNECTION_ERROR_MSG}") return None except requests.exceptions.Timeout: - console.print(f"\n{CONNECTION_TIMEOUT_MSG}") + console_print(f"\n{CONNECTION_TIMEOUT_MSG}") + return None + except Exception: + console_print("[red]Failed to list your routines.[/red]") + return None + + +def get_default_routines( + url: str = DEFAULT_ROUTINES_URL, timeout: int = TIMEOUT, silent: bool = False +): + """Get the default routines from CMS. + + Parameters + ---------- + timeout : int + The timeout, by default TIMEOUT + silent : bool + Whether to silence the console output, by default False + + Returns + ------- + Optional[requests.Response] + The response from the get request. + """ + console_print = console.print if not silent else lambda *args, **kwargs: None + try: + response = requests.get( + url=url, + timeout=timeout, + ) + if response.status_code != 200: + console_print("[red]Failed to get default routines.[/red]") + return response + except requests.exceptions.ConnectionError: + console_print(f"\n{CONNECTION_ERROR_MSG}") + return None + except requests.exceptions.Timeout: + console_print(f"\n{CONNECTION_TIMEOUT_MSG}") return None except Exception: - console.print("[red]Failed to list your routines.[/red]") + console_print("[red]Failed to get default routines.[/red]") return None diff --git a/openbb_terminal/core/session/local_model.py b/openbb_terminal/core/session/local_model.py index f5b5c4f02b38..8a497560791e 100644 --- a/openbb_terminal/core/session/local_model.py +++ b/openbb_terminal/core/session/local_model.py @@ -10,7 +10,6 @@ ) from openbb_terminal.core.models.sources_model import get_allowed_sources from openbb_terminal.core.session.current_user import ( - get_current_user, get_env_dict, set_credential, set_preference, @@ -87,11 +86,17 @@ def remove(path: Path) -> bool: return False -def update_flair(): - """Update the flair.""" +def update_flair(username: str): + """Update the flair. + + Parameters + ---------- + username : str + The username. + """ if "FLAIR" not in get_env_dict(): MAX_FLAIR_LEN = 20 - flair = "[" + get_current_user().profile.username[:MAX_FLAIR_LEN] + "]" + " 🦋" + flair = "[" + username[:MAX_FLAIR_LEN] + "]" + " 🦋" set_preference("FLAIR", flair) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/core/session/routines_handler.py similarity index 55% rename from openbb_terminal/account/account_model.py rename to openbb_terminal/core/session/routines_handler.py index 3c7543f6ca3f..75cb726ff401 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/core/session/routines_handler.py @@ -1,65 +1,58 @@ import os from pathlib import Path -from typing import Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import numpy as np import pandas as pd +import openbb_terminal.core.session.hub_model as Hub from openbb_terminal.core.session.current_user import get_current_user from openbb_terminal.rich_config import console -__login_called = False - -def get_login_called(): - """Get the login/logout called flag. - - Returns - ------- - bool - The login/logout called flag. - """ - return __login_called - - -def set_login_called(value: bool): - """Set the login/logout called flag. +def download_routines(auth_header: str, silent: bool = False) -> Dict[str, str]: + """Download default and personal routines. Parameters ---------- - value : bool - The login/logout called flag. - """ - global __login_called # pylint: disable=global-statement - __login_called = value - - -def get_routines_info(response) -> Tuple[pd.DataFrame, int, int]: - """Get the routines list. - - Parameters - ---------- - response : requests.Response - The response. + auth_header : str + The authorization header, e.g. "Bearer ". + silent : bool + Whether to silence the console output, by default False Returns ------- - Tuple[pd.DataFrame, int, int] - The routines list, the current page and the total number of pages. + Dict[str, str] + The routines. """ - df = pd.DataFrame() - page = 1 - pages = 1 + routines_dict = {} + + response = Hub.get_default_routines(silent=silent) if response and response.status_code == 200: - data = response.json() - page = data.get("page", 1) - pages = data.get("pages", 1) - items = data.get("items", []) - if items: - df = pd.DataFrame(items) - df.index = np.arange(1, len(df) + 1) + content = response.json() + data = content.get("data", []) + for routine in data: + name = routine.get("name", "") + if name: + routines_dict[name] = routine.get("script", "") + + # Number of routines downloaded is limited to 100 + response = Hub.list_routines( + auth_header=auth_header, + fields=["name", "script"], + page=1, + size=100, + silent=silent, + ) + if response and response.status_code == 200: + content = response.json() + items = content.get("items", []) + for routine in items: + name = routine.get("name", "") + if name: + routines_dict[name] = routine.get("script", "") - return df, page, pages + return routines_dict def read_routine(file_name: str, folder: Optional[Path] = None) -> Optional[str]: @@ -105,6 +98,7 @@ def save_routine( routine: str, folder: Optional[Path] = None, force: bool = False, + silent: bool = False, ) -> Union[Optional[Path], str]: """Save the routine. @@ -118,12 +112,15 @@ def save_routine( The routines folder. force : bool Force the save. + silent : bool + Whether to silence the console output, by default False Returns ------- Optional[Path, str] The path to the routine or None. """ + console_print = console.print if not silent else lambda *args, **kwargs: None current_user = get_current_user() if folder is None: @@ -141,5 +138,57 @@ def save_routine( f.write(routine) return user_folder / file_name except Exception: - console.print("[red]\nFailed to save routine.[/red]") + console_print("[red]\nFailed to save routine.[/red]") return None + + +def get_default_routines_info(routines: List[Dict[str, str]]) -> pd.DataFrame: + """Get the routines list. + + Parameters + ---------- + response : requests.Response + The response. + + Returns + ------- + Tuple[pd.DataFrame, int, int] + The routines list, the current page and the total number of pages. + """ + df = pd.DataFrame() + if routines: + df = pd.DataFrame(routines) + if all( + c in df.columns for c in ["name", "description", "version", "date_updated"] + ): + df = df[["name", "description", "version", "date_updated"]] + df.index = np.arange(1, len(df) + 1) + return df + + +def get_personal_routines_info(response) -> Tuple[pd.DataFrame, int, int]: + """Get the routines list. + + Parameters + ---------- + response : requests.Response + The response. + + Returns + ------- + Tuple[pd.DataFrame, int, int] + The routines list, the current page and the total number of pages. + """ + df = pd.DataFrame() + page = 1 + pages = 1 + if response and response.status_code == 200: + data = response.json() + page = data.get("page", 1) + pages = data.get("pages", 1) + items = data.get("items", []) + if items: + df = pd.DataFrame(items) + df.index = np.arange(1, len(df) + 1) + + return df, page, pages diff --git a/openbb_terminal/core/session/session_controller.py b/openbb_terminal/core/session/session_controller.py index c30e4a54a853..d14021e610e7 100644 --- a/openbb_terminal/core/session/session_controller.py +++ b/openbb_terminal/core/session/session_controller.py @@ -79,6 +79,7 @@ def prompt(welcome=True): def launch_terminal(): """Launch terminal""" + # pylint: disable=import-outside-toplevel from openbb_terminal import terminal_controller terminal_controller.parse_args_and_run() diff --git a/openbb_terminal/core/session/session_model.py b/openbb_terminal/core/session/session_model.py index f41c1cbc9e86..b6962707f48a 100644 --- a/openbb_terminal/core/session/session_model.py +++ b/openbb_terminal/core/session/session_model.py @@ -20,6 +20,10 @@ set_current_user, set_default_user, ) +from openbb_terminal.core.session.routines_handler import ( + download_routines, + save_routine, +) from openbb_terminal.core.session.sources_handler import get_updated_hub_sources from openbb_terminal.core.session.utils import run_thread from openbb_terminal.helper_funcs import system_clear @@ -99,21 +103,15 @@ def login(session: dict) -> LoginStatus: email = configs.get("email", "") hub_user.profile.load_user_info(session, email) set_current_user(hub_user) - Local.apply_configs(configs=configs) - Local.update_flair() - - # Update user sources in backend - updated_sources = get_updated_hub_sources(configs) - if updated_sources: - run_thread( - Hub.upload_user_field, - { - "key": "features_sources", - "value": updated_sources, - "auth_header": get_current_user().profile.get_auth_header(), - "silent": True, - }, - ) + + auth_header = hub_user.profile.get_auth_header() + run_thread(download_and_save_routines, {"auth_header": auth_header}) + run_thread( + update_backend_sources, {"auth_header": auth_header, "configs": configs} + ) + Local.apply_configs(configs) + Local.update_flair(get_current_user().profile.username) + return LoginStatus.SUCCESS if response.status_code == 401: return LoginStatus.UNAUTHORIZED @@ -121,6 +119,50 @@ def login(session: dict) -> LoginStatus: return LoginStatus.NO_RESPONSE +def download_and_save_routines(auth_header: str, silent: bool = True): + """Download and save routines. + + Parameters + ---------- + auth_header : str + The authorization header, e.g. "Bearer ". + silent : bool + Whether to silence the console output, by default True + """ + routines = download_routines(auth_header=auth_header, silent=silent) + for name, content in routines.items(): + save_routine( + file_name=f"{name}.openbb", routine=content, force=True, silent=silent + ) + + +def update_backend_sources(auth_header, configs, silent: bool = True): + """Update backend sources if new source or command path available. + + Parameters + ---------- + auth_header : str + The authorization header, e.g. "Bearer ". + configs : Dict + Dictionary with configs + silent : bool + Whether to silence the console output, by default True + """ + console_print = console.print if not silent else lambda *args, **kwargs: None + + try: + updated_sources = get_updated_hub_sources(configs) + if updated_sources: + Hub.upload_user_field( + key="features_sources", + value=updated_sources, + auth_header=auth_header, + silent=silent, + ) + except Exception: + console_print("[red]Failed to update backend sources.[/red]") + + def logout( auth_header: Optional[str] = None, token: Optional[str] = None, diff --git a/openbb_terminal/core/session/utils.py b/openbb_terminal/core/session/utils.py index 04582817475a..45ab3e509eef 100644 --- a/openbb_terminal/core/session/utils.py +++ b/openbb_terminal/core/session/utils.py @@ -1,5 +1,5 @@ from threading import Thread -from typing import Any, Type, TypeVar +from typing import Any, Callable, Dict, Type, TypeVar from pydantic import ValidationError @@ -44,15 +44,15 @@ def load_dict_to_model(dictionary: dict, model: Type[T]) -> T: return model() # type: ignore -def run_thread(target, kwargs): - """Run a thread. +def run_thread(target: Callable, kwargs: Dict[str, Any]): + """Run a daemon thread, with the given target and keyword arguments. Parameters ---------- - target : function + target : Callable The target function. - args : tuple - The arguments. + kwargs : Dict[str, Any] + The keyword arguments. """ - thread = Thread(target=target, kwargs=kwargs) + thread = Thread(target=target, kwargs=kwargs, daemon=True) thread.start() diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index af90c146fe51..d3ddd4fefb01 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -24,7 +24,7 @@ from rich import panel import openbb_terminal.config_terminal as cfg -from openbb_terminal.account.account_model import ( +from openbb_terminal.account.account_controller import ( get_login_called, set_login_called, ) @@ -261,13 +261,13 @@ def call_guess(self, other_args: List[str]) -> None: current_user = get_current_user() if self.GUESS_NUMBER_TRIES_LEFT == 0 and self.GUESS_SUM_SCORE < 0.01: - parser_exe = argparse.ArgumentParser( + parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="guess", description="Guess command to achieve task successfully.", ) - parser_exe.add_argument( + parser.add_argument( "-l", "--limit", type=check_positive, @@ -277,7 +277,7 @@ def call_guess(self, other_args: List[str]) -> None: ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-l") - ns_parser_guess = self.parse_simple_args(parser_exe, other_args) + ns_parser_guess = self.parse_simple_args(parser, other_args) if self.GUESS_TOTAL_TRIES == 0: self.GUESS_NUMBER_TRIES_LEFT = ns_parser_guess.limit @@ -520,31 +520,12 @@ def call_exe(self, other_args: List[str]): if not other_args: console.print( - "[red]Provide a path to the routine you wish to execute. For an example, please use " + "[info]Provide a path to the routine you wish to execute. For an example, please use " "`exe --example` and for documentation and to learn how create your own script " - "type `about exe`.\n[/red]" + "type `about exe`.\n[/info]" ) return - - full_input = " ".join(other_args) - other_args_processed = ( - full_input.split(" ") if " " in full_input else [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( + parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="exe", @@ -552,20 +533,25 @@ def call_exe(self, other_args: List[str]): "`exe --example` and for documentation and to learn how create your own script " "type `about exe`.", ) - parser_exe.add_argument( + parser.add_argument( "--file", help="The path or .openbb file to run.", - dest="path", - default=None, + dest="file", + required="-h" not in other_args + and "--help" not in other_args + and "-e" not in other_args + and "--example" not in other_args, + type=str, + nargs="+", ) - parser_exe.add_argument( + 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(",")], ) - parser_exe.add_argument( + parser.add_argument( "-e", "--example", help="Run an example script to understand how routines can be used.", @@ -573,25 +559,23 @@ def call_exe(self, other_args: List[str]): action="store_true", default=False, ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "--file") + ns_parser = self.parse_known_args_and_warn(parser, other_args) - if not args[0]: - return console.print("[red]Please select an .openbb routine file.[/red]\n") - - if args and "-" not in args[0][0]: - args.insert(0, "--file") - ns_parser_exe = self.parse_simple_args(parser_exe, args) - if ns_parser_exe and (ns_parser_exe.path or ns_parser_exe.example): - if ns_parser_exe.example: + if ns_parser: + if ns_parser.example: path = MISCELLANEOUS_DIRECTORY / "routines" / "routine_example.openbb" console.print( - "[green]Executing an example, please type `about exe` " - "to learn how to create your own script.[/green]\n" + "[info]Executing an example, please type `about exe` " + "to learn how to create your own script.[/info]\n" ) time.sleep(3) - elif ns_parser_exe.path in self.ROUTINE_CHOICES["--file"]: - path = self.ROUTINE_FILES[ns_parser_exe.path] + elif ns_parser.file: + file_path = " ".join(ns_parser.file) + path = self.ROUTINE_FILES.get(file_path, Path(file_path)) else: - path = ns_parser_exe.path + return with open(path) as fp: raw_lines = [ @@ -610,8 +594,8 @@ def call_exe(self, other_args: List[str]): # 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): + if ns_parser.routine_args: + for i, arg in enumerate(ns_parser.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) diff --git a/tests/openbb_terminal/account/test_account_controller.py b/tests/openbb_terminal/account/test_account_controller.py index c263f31097ea..1ad58884737a 100644 --- a/tests/openbb_terminal/account/test_account_controller.py +++ b/tests/openbb_terminal/account/test_account_controller.py @@ -46,9 +46,24 @@ ROUTINES = { "items": [ - {"name": "scrip1", "description": "abc"}, - {"name": "script2", "description": "def"}, - {"name": "script3", "description": "ghi"}, + { + "name": "scrip1", + "description": "abc", + "version": "0.0.0", + "updated_date": "2021-01-01", + }, + { + "name": "script2", + "description": "def", + "version": "0.0.1", + "updated_date": "2022-01-01", + }, + { + "name": "script3", + "description": "ghi", + "version": "0.0.2", + "updated_date": "2023-01-01", + }, ], "total": 3, "page": 1, @@ -70,6 +85,15 @@ def vcr_config(): } +@pytest.fixture(autouse=True) +def fetch_routines(mocker): + path_controller = "openbb_terminal.account.account_controller" + mocker.patch( + target=f"{path_controller}.AccountController.fetch_default_routines", + return_value=[], + ) + + @pytest.fixture(name="test_user") def fixture_test_user(mocker): mocker.patch( @@ -187,11 +211,12 @@ def __call__(self, *args, **kwargs): @pytest.mark.vcr(record_mode="none") @pytest.mark.record_stdout def test_print_help(mocker, test_user): - controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" mocker.patch( - target="openbb_terminal.account.account_controller.get_current_user", + target=f"{path_controller}.get_current_user", return_value=test_user, ) + controller = account_controller.AccountController(queue=None) controller.print_help() diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt index d550119c1f93..a702c294c8f5 100644 --- a/tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt @@ -1,4 +1,6 @@ - name description -1 scrip1 abc -2 script2 def -3 script3 ghi + name description version updated_date +1 scrip1 abc 0.0.0 2021-01-01 00:00:00 +2 script2 def 0.0.1 2022-01-01 00:00:00 +3 script3 ghi 0.0.2 2023-01-01 00:00:00 + +Failed to display default routines. diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt deleted file mode 100644 index 4edd4cdac07b..000000000000 --- a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt +++ /dev/null @@ -1 +0,0 @@ -[info]sync:[/info] OFF diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt deleted file mode 100644 index 2be0167f17b4..000000000000 --- a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt +++ /dev/null @@ -1 +0,0 @@ -[info]sync:[/info] ON diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt deleted file mode 100644 index 2be0167f17b4..000000000000 --- a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt +++ /dev/null @@ -1 +0,0 @@ -[info]sync:[/info] ON diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt deleted file mode 100644 index 4edd4cdac07b..000000000000 --- a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt +++ /dev/null @@ -1 +0,0 @@ -[info]sync:[/info] OFF diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt deleted file mode 100644 index e7dcaab864b1..000000000000 --- a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt +++ /dev/null @@ -1 +0,0 @@ -sync is ON, use --on or --off to change. diff --git a/tests/openbb_terminal/session/test_hub_model.py b/tests/openbb_terminal/session/test_hub_model.py index c1805e382050..ee8bbf524a83 100644 --- a/tests/openbb_terminal/session/test_hub_model.py +++ b/tests/openbb_terminal/session/test_hub_model.py @@ -702,7 +702,7 @@ def test_list_routines(auth_header, page, size, base_url, timeout, status_code): assert ( kwargs["url"] == base_url - + f"terminal/script?fields=name%2Cdescription&page={page}&size={size}" + + f"terminal/script?fields=name%2Cdescription%2Cversion%2Cupdated_date&page={page}&size={size}" ) assert kwargs["headers"] == {"Authorization": auth_header} assert kwargs["timeout"] == timeout diff --git a/tests/openbb_terminal/account/test_account_model.py b/tests/openbb_terminal/session/test_routines_handler.py similarity index 92% rename from tests/openbb_terminal/account/test_account_model.py rename to tests/openbb_terminal/session/test_routines_handler.py index 4e2306cd2ef0..8ea78b16ebe0 100644 --- a/tests/openbb_terminal/account/test_account_model.py +++ b/tests/openbb_terminal/session/test_routines_handler.py @@ -8,7 +8,6 @@ import pytest # IMPORTATION INTERNAL -from openbb_terminal.account.account_model import read_routine, save_routine from openbb_terminal.core.models.user_model import ( CredentialsModel, PreferencesModel, @@ -17,6 +16,7 @@ UserModel, ) from openbb_terminal.core.session.current_user import get_current_user +from openbb_terminal.core.session.routines_handler import read_routine, save_routine @pytest.fixture(name="test_user") @@ -40,7 +40,7 @@ def test_read_routine(mocker, exists: bool, test_user): file_name = "test_routine.openbb" routine = "do something" current_user = get_current_user() - path = "openbb_terminal.account.account_model" + path = "openbb_terminal.core.session.routines_handler" mocker.patch( target=path + ".get_current_user", @@ -71,7 +71,7 @@ def test_read_routine(mocker, exists: bool, test_user): def test_read_routine_exception(mocker, test_user): file_name = "test_routine.openbb" current_user = get_current_user() - path = "openbb_terminal.account.account_model" + path = "openbb_terminal.core.session.routines_handler" mocker.patch( target=path + ".get_current_user", @@ -104,7 +104,7 @@ def test_save_routine(mocker, exists: bool, test_user): file_name = "test_routine.openbb" routine = "do something" current_user = get_current_user() - path = "openbb_terminal.account.account_model" + path = "openbb_terminal.core.session.routines_handler" mocker.patch( target=path + ".get_current_user", @@ -138,7 +138,7 @@ def test_save_routine(mocker, exists: bool, test_user): def test_save_routine_exception(mocker, test_user): file_name = "test_routine.openbb" routine = "do something" - path = "openbb_terminal.account.account_model" + path = "openbb_terminal.core.session.routines_handler" mocker.patch( target=path + ".get_current_user",