mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-14 05:14:19 +00:00
* I missed some sheet_name ones :) * Move quote to main menu and combine yahoo finance commands * Update stocks for quote * First update to move yfinance away from main functionality * Small updates * Update tests to reflect changes * Fix some little things * Update docs a little too * Remove the Due Diligence menu * Update tests for new change * Update fa and stocks with new text and move some stuff around * Bye bye dd * Pls work * work.. * work.. * work.. * Work..| * Add tests back in * Remove tests * Fix fix fix * Remove tests to fix conflicts * Improve the `stocks/search` command (#4084) * Improve search command * Make it more intuitive * Fix tests * Update the docs * Ok done * Ok done 2 * Ok done 3 * Git update tests that failed * Update commands with --sheet-name that didn't work (#4088) * Update commands with --sheet-name that didn't work * Fix test * oops * Adds auto completion to `news` (#4089) * Fix plot look when using eval and add query to choices (#3881) * convert index to datetime and update choices * uncomment economy integration test * fix treasury concat bug * fix datasets concat on duplciates * Lock ruff version so that new lints dont break our CI (#3905) * Lock ruff version so that new lints dont break our CI * Bumped pre-commit ruff version * returning always a dataframe * Revert "returning always a dataframe" This reverts commitd5dbf3132f. * quick fix to auto complete args --------- Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Co-authored-by: James Maslek <jmaslek11@gmail.com> * Hotfix/Windows-CLI (#4087) * init * Update windows10_build.yml * Update windows10_build.yml * Update windows10_build.yml * Update windows10_build.yml * Update windows10_build.yml * Update windows10_build.yml * oops --------- Co-authored-by: James Maslek <jmaslek11@gmail.com> * Adds end date to `forex/load` (#4077) * Fix plot look when using eval and add query to choices (#3881) * convert index to datetime and update choices * uncomment economy integration test * fix treasury concat bug * fix datasets concat on duplciates * Lock ruff version so that new lints dont break our CI (#3905) * Lock ruff version so that new lints dont break our CI * Bumped pre-commit ruff version * returning always a dataframe * Revert "returning always a dataframe" This reverts commitd5dbf3132f. * adding end date to the forex load * added some tests * added example * always show msg * rewriting test * updating tests * updating tests * Redo load tests --------- Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Co-authored-by: James Maslek <jmaslek11@gmail.com> * Update tests to reflect changes * work.. * Remove tests * Fix fix fix * Remove tests * Update the whole test folder * Small updates * Revert change * Update eodhd to reflect it requires a paid plan and update tests * Fix all the tests * Update tests * Update integration tests * Removing tests so no whining happens * ADD ALL THE TESTS * Fix linting * gitignore. thanks jer * Add back dd and redirect to fa * Update to just display a message * Update OpenBB SDK commands * Fixing issues with exporting * Fix ruff messing up * Fix tests * Fix the tests by disabling isort to run on tests folder --------- Co-authored-by: Jeroen Bouma <jeroenbouma@Jeroens-MacBook-Pro.local> Co-authored-by: James Maslek <jmaslek11@gmail.com> Co-authored-by: Henrique Joaquim <h.joaquim@campus.fct.unl.pt> Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Co-authored-by: teh_coderer <me@tehcoderer.com>
322 lines
11 KiB
Python
322 lines
11 KiB
Python
"""Rich Module"""
|
|
__docformat__ = "numpy"
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Tuple
|
|
|
|
import i18n
|
|
from rich import panel
|
|
from rich.console import Console, Theme
|
|
from rich.text import Text
|
|
|
|
from openbb_terminal import (
|
|
config_terminal as cfg,
|
|
feature_flags as obbff,
|
|
)
|
|
from openbb_terminal.core.config.paths import MISCELLANEOUS_DIRECTORY
|
|
|
|
# pylint: disable=no-member,c-extension-no-member
|
|
|
|
|
|
# https://rich.readthedocs.io/en/stable/appendix/colors.html#appendix-colors
|
|
# https://rich.readthedocs.io/en/latest/highlighting.html#custom-highlighters
|
|
|
|
CUSTOM_THEME = Theme(cfg.theme.console_style)
|
|
|
|
RICH_TAGS = [
|
|
"[menu]",
|
|
"[/menu]",
|
|
"[cmds]",
|
|
"[/cmds]",
|
|
"[info]",
|
|
"[/info]",
|
|
"[param]",
|
|
"[/param]",
|
|
"[src]",
|
|
"[/src]",
|
|
"[help]",
|
|
"[/help]",
|
|
]
|
|
|
|
USE_COLOR = True
|
|
|
|
|
|
def translate(key: str):
|
|
return i18n.t(key)
|
|
|
|
|
|
def no_panel(renderable, *args, **kwargs): # pylint: disable=unused-argument
|
|
return renderable
|
|
|
|
|
|
def get_ordered_list_sources(command_path: str):
|
|
"""
|
|
Returns the preferred source for the given command. If a value is not available for the specific
|
|
command, returns the most specific source, eventually returning the overall default source.
|
|
|
|
Parameters
|
|
----------
|
|
command_path: str
|
|
The command to find the source for. Example would be "stocks/load" to return the value
|
|
for stocks.load first, then stocks, then the default value.
|
|
|
|
Returns
|
|
-------
|
|
list:
|
|
list of sources
|
|
"""
|
|
try:
|
|
# Loading in both source files: default sources and user sources
|
|
default_data_source = MISCELLANEOUS_DIRECTORY / "data_sources_default.json"
|
|
user_data_source = Path(obbff.PREFERRED_DATA_SOURCE_FILE)
|
|
|
|
# Opening default sources file from the repository root
|
|
with open(str(default_data_source)) as json_file:
|
|
json_doc = json.load(json_file)
|
|
|
|
# If the user has added sources to their own sources file in OpenBBUserData, then use that
|
|
if (
|
|
not os.getenv("TEST_MODE")
|
|
and user_data_source.exists()
|
|
and user_data_source.stat().st_size > 0
|
|
):
|
|
with open(str(user_data_source)) as json_file:
|
|
json_doc = json.load(json_file)
|
|
|
|
# We are going to iterate through each command as if it is broken up by period characters (.)
|
|
path_objects = command_path.split("/")[1:]
|
|
|
|
# Start iterating through the top-level JSON doc to start
|
|
deepest_level = json_doc
|
|
|
|
# If we still have entries in path_objects, continue to go deeper
|
|
while len(path_objects) > 0:
|
|
# Is this path object in the JSON doc? If so, go into that for our next iteration.
|
|
if path_objects[0] in deepest_level:
|
|
# We found the element, so go one level deeper
|
|
deepest_level = deepest_level[path_objects[0]]
|
|
|
|
else:
|
|
# If we have not find the `load` on the deepest level it means we may be in a sub-menu
|
|
# and we can use the load from the Base class
|
|
if path_objects[0] == "load":
|
|
# Get the context associated with the sub-menu (e.g. stocks, crypto, ...)
|
|
context = command_path.split("/")[1]
|
|
|
|
# Grab the load source from that context if it exists, otherwise throws an error
|
|
if context in json_doc:
|
|
if "load" in json_doc[context]:
|
|
return json_doc[context]["load"]
|
|
|
|
# We didn't find the next level, so flag that that command default source is missing
|
|
# Which means that there aren't more than 1 source and therefore no selection is necessary
|
|
return []
|
|
|
|
# Go one level deeper into the path
|
|
path_objects = path_objects[1:]
|
|
|
|
# We got through all values, so return this as the final value
|
|
return deepest_level
|
|
|
|
except Exception as e:
|
|
console.print(
|
|
f"[red]Failed to load preferred source from file: "
|
|
f"{obbff.PREFERRED_DATA_SOURCE_FILE}[/red]"
|
|
)
|
|
console.print(f"[red]{e}[/red]")
|
|
return None
|
|
|
|
|
|
class MenuText:
|
|
"""Create menu text with rich colors to be displayed by terminal"""
|
|
|
|
def __init__(self, path: str = "", column_sources: int = 100):
|
|
"""Initialize menu help
|
|
|
|
Parameters
|
|
----------
|
|
path : str
|
|
path to the menu that is being created
|
|
column_sources : int
|
|
column width from which to start displaying sources
|
|
"""
|
|
self.menu_text = ""
|
|
self.menu_path = path
|
|
self.col_src = column_sources
|
|
|
|
def add_raw(self, raw_text: str):
|
|
"""Append raw text (no translation) to a menu
|
|
|
|
Parameters
|
|
----------
|
|
raw_text : str
|
|
raw text to be appended to the menu
|
|
"""
|
|
self.menu_text += raw_text
|
|
|
|
def add_custom(self, key: str):
|
|
"""Append custom text (after translation from key) to a menu
|
|
|
|
Parameters
|
|
----------
|
|
key : str
|
|
key to get translated text and add to the menu
|
|
"""
|
|
self.menu_text += f"{i18n.t(self.menu_path + key)}"
|
|
|
|
def add_info(self, key_info: str):
|
|
"""Append info text (after translation from key) to a menu
|
|
|
|
Parameters
|
|
----------
|
|
key_info : str
|
|
key to get translated text and add to the menu as info
|
|
"""
|
|
self.menu_text += f"[info]{i18n.t(self.menu_path + key_info)}:[/info]\n"
|
|
|
|
def add_param(self, key_param: str, value: str, col_align: int = 0):
|
|
"""Append info text (after translation from key) to a menu
|
|
|
|
Parameters
|
|
----------
|
|
key_param : str
|
|
key to get translated text and add to the menu as parameter
|
|
value : str
|
|
value to display in front of the parameter
|
|
col_align : int
|
|
column alignment for the value. This allows for a better UX experience.
|
|
"""
|
|
parameter_translated = i18n.t(self.menu_path + key_param)
|
|
if col_align > len(parameter_translated):
|
|
space = (col_align - len(parameter_translated)) * " "
|
|
else:
|
|
space = ""
|
|
self.menu_text += f"[param]{parameter_translated}{space}:[/param] {value}\n"
|
|
|
|
def add_cmd(self, key_command: str, condition: bool = True):
|
|
"""Append command text (after translation from key) to a menu
|
|
|
|
Parameters
|
|
----------
|
|
key_command : str
|
|
key command to be executed by user. It is also used as a key to get description of command.
|
|
condition : bool
|
|
condition in which command is available to user. I.e. displays command and description.
|
|
If condition is false, the command line is greyed out.
|
|
"""
|
|
spacing = (23 - (len(key_command) + 4)) * " "
|
|
if condition:
|
|
cmd = f"[cmds] {key_command}{spacing}{i18n.t(self.menu_path + key_command)}[/cmds]"
|
|
else:
|
|
cmd = f"[unvl] {key_command}{spacing}{i18n.t(self.menu_path + key_command)}[/unvl]"
|
|
|
|
sources = get_ordered_list_sources(f"/{self.menu_path}{key_command}")
|
|
|
|
if sources:
|
|
if self.col_src > len(cmd):
|
|
space = (self.col_src - len(cmd)) * " "
|
|
else:
|
|
space = " "
|
|
cmd += f"{space}[src][{', '.join(sources)}][/src]"
|
|
|
|
self.menu_text += cmd + "\n"
|
|
|
|
def add_menu(self, key_menu: str, condition: bool = True):
|
|
"""Append menu text (after translation from key) to a menu
|
|
|
|
Parameters
|
|
----------
|
|
key_menu : str
|
|
key menu to be executed by user. It is also used as a key to get description of menu.
|
|
condition : bool
|
|
condition in which menu is available to user. I.e. displays menu and description.
|
|
If condition is false, the menu line is greyed out.
|
|
"""
|
|
spacing = (23 - (len(key_menu) + 4)) * " "
|
|
if condition:
|
|
self.menu_text += f"[menu]> {key_menu}{spacing}{i18n.t(self.menu_path + key_menu)}[/menu]\n"
|
|
else:
|
|
self.menu_text += f"[unvl]> {key_menu}{spacing}{i18n.t(self.menu_path + key_menu)}[/unvl]\n"
|
|
|
|
def add_setting(self, key_setting: str, status: bool = True):
|
|
"""Append menu text (after translation from key) to a menu
|
|
|
|
Parameters
|
|
----------
|
|
key_setting : str
|
|
key setting to be set by user. It is also used as a key to get description of the setting.
|
|
status : bool
|
|
status of the current setting. If true the line will be green, otherwise red.
|
|
"""
|
|
spacing = (23 - (len(key_setting) + 4)) * " "
|
|
if status:
|
|
self.menu_text += f"[green] {key_setting}{spacing}{i18n.t(self.menu_path + key_setting)}[/green]\n"
|
|
else:
|
|
self.menu_text += f"[red] {key_setting}{spacing}{i18n.t(self.menu_path + key_setting)}[/red]\n"
|
|
|
|
|
|
class ConsoleAndPanel:
|
|
"""Create a rich console to wrap the console print with a Panel"""
|
|
|
|
def __init__(self):
|
|
self.console = Console(theme=CUSTOM_THEME, highlight=False, soft_wrap=True)
|
|
self.menu_text = ""
|
|
self.menu_path = ""
|
|
|
|
def capture(self):
|
|
return self.console.capture()
|
|
|
|
@staticmethod
|
|
def filter_rich_tags(text):
|
|
for val in RICH_TAGS:
|
|
text = text.replace(val, "")
|
|
|
|
return text
|
|
|
|
@staticmethod
|
|
def blend_text(
|
|
message: str, color1: Tuple[int, int, int], color2: Tuple[int, int, int]
|
|
) -> Text:
|
|
"""Blend text from one color to another."""
|
|
text = Text(message)
|
|
r1, g1, b1 = color1
|
|
r2, g2, b2 = color2
|
|
dr = r2 - r1
|
|
dg = g2 - g1
|
|
db = b2 - b1
|
|
size = len(text) + 5
|
|
for index in range(size):
|
|
blend = index / size
|
|
color = f"#{int(r1 + dr * blend):02X}{int(g1 + dg * blend):02X}{int(b1 + db * blend):02X}"
|
|
text.stylize(color, index, index + 1)
|
|
return text
|
|
|
|
def print(self, *args, **kwargs):
|
|
if kwargs and "text" in list(kwargs) and "menu" in list(kwargs):
|
|
if not os.getenv("TEST_MODE"):
|
|
if obbff.ENABLE_RICH_PANEL:
|
|
version = f"[param]OpenBB Terminal v{obbff.VERSION}[/param] (https://openbb.co)"
|
|
self.console.print(
|
|
panel.Panel(
|
|
"\n" + kwargs["text"],
|
|
title=kwargs["menu"],
|
|
subtitle_align="right",
|
|
subtitle=version,
|
|
)
|
|
)
|
|
|
|
else:
|
|
self.console.print(kwargs["text"])
|
|
else:
|
|
print(self.filter_rich_tags(kwargs["text"]))
|
|
else:
|
|
if not os.getenv("TEST_MODE"):
|
|
self.console.print(*args, **kwargs)
|
|
else:
|
|
print(*args, **kwargs)
|
|
|
|
|
|
console = ConsoleAndPanel()
|