[Feature] - Rename terminal folder (#6349)

* rename terminal folder

* docstring

* Rename to openbb_cli

* rename constant

* rename .his file

* pyproject.toml

* Update pre-commit and remove docker-build.yml
This commit is contained in:
montezdesousa
2024-04-30 13:42:31 +01:00
committed by GitHub
parent 5f4ee56979
commit cc15a770da
51 changed files with 162 additions and 221 deletions

View File

@@ -0,0 +1 @@
"""Package init"""

View File

@@ -0,0 +1,148 @@
import inspect
from typing import Any, Dict, Optional, Type
# TODO: this needs to be done differently
from openbb_core.app.static.container import Container
from openbb_cli.argparse_translator.argparse_translator import (
ArgparseTranslator,
ReferenceToCustomArgumentsProcessor,
)
class ArgparseClassProcessor:
"""
Process a target class to create ArgparseTranslators for its methods.
"""
# reference variable used to create custom groups for the ArgpaseTranslators
_reference: Dict[str, Any] = {}
def __init__(
self,
target_class: Type,
add_help: bool = False,
reference: Optional[Dict[str, Any]] = None,
):
"""
Initialize the ArgparseClassProcessor.
Parameters
----------
target_class : Type
The target class whose methods will be processed.
add_help : Optional[bool]
Whether to add help to the ArgparseTranslators.
"""
self._target_class: Type = target_class
self._add_help: bool = add_help
self._translators: Dict[str, ArgparseTranslator] = {}
self._paths: Dict[str, str] = {}
ArgparseClassProcessor._reference = reference or {}
self._translators = self._process_class(
target=self._target_class, add_help=self._add_help
)
self._paths[self._get_class_name(self._target_class)] = "path"
self._build_paths(target=self._target_class)
@property
def translators(self) -> Dict[str, ArgparseTranslator]:
"""
Get the ArgparseTranslators associated with the target class.
Returns
-------
Dict[str, ArgparseTranslator]
The ArgparseTranslators associated with the target class.
"""
return self._translators
@property
def paths(self) -> Dict[str, str]:
"""
Get the paths associated with the target class.
Returns
-------
Dict[str, str]
The paths associated with the target class.
"""
return self._paths
@classmethod
def _custom_groups_from_reference(cls, class_name: str, function_name: str) -> Dict:
route = f"/{class_name.replace('_', '/')}/{function_name}"
reference = {route: cls._reference[route]} if route in cls._reference else {}
if not reference:
return {}
rp = ReferenceToCustomArgumentsProcessor(reference)
return rp.custom_groups.get(route, {}) # type: ignore
@classmethod
def _process_class(
cls,
target: type,
add_help: bool = False,
) -> Dict[str, ArgparseTranslator]:
methods = {}
for name, member in inspect.getmembers(target):
if name.startswith("__") or name.startswith("_"):
continue
if inspect.ismethod(member):
class_name = cls._get_class_name(target)
methods[f"{class_name}_{name}"] = ArgparseTranslator(
func=member,
add_help=add_help,
custom_argument_groups=cls._custom_groups_from_reference( # type: ignore
class_name=class_name, function_name=name
),
)
elif isinstance(member, Container):
methods = {
**methods,
**cls._process_class(
target=getattr(target, name), add_help=add_help
),
}
return methods
@staticmethod
def _get_class_name(target: type) -> str:
return (
str(type(target))
.rsplit(".", maxsplit=1)[-1]
.replace("'>", "")
.replace("ROUTER_", "")
.lower()
)
def get_translator(self, command: str) -> ArgparseTranslator:
"""
Retrieve the ArgparseTranslator object associated with a specific menu and command.
Parameters
----------
command : str
The command associated with the ArgparseTranslator.
Returns
-------
ArgparseTranslator
The ArgparseTranslator associated with the specified menu and command.
"""
return self._translators[command]
def _build_paths(self, target: type, depth: int = 1):
for name, member in inspect.getmembers(target):
if name.startswith("__") or name.startswith("_"):
continue
if inspect.ismethod(member):
pass
elif isinstance(member, Container):
self._build_paths(target=getattr(target, name), depth=depth + 1)
self._paths[f"{name}"] = "sub" * depth + "path"

View File

@@ -0,0 +1,530 @@
import argparse
import inspect
from copy import deepcopy
from enum import Enum
from typing import (
Any,
Callable,
Dict,
List,
Literal,
Optional,
Tuple,
Type,
Union,
get_args,
get_origin,
get_type_hints,
)
from openbb_core.app.model.field import OpenBBField
from pydantic import BaseModel, model_validator
from typing_extensions import Annotated
SEP = "__"
class ArgparseActionType(Enum):
store = "store"
store_true = "store_true"
class CustomArgument(BaseModel):
name: str
type: Optional[Any]
dest: str
default: Any
required: bool
action: Literal["store_true", "store"]
help: str
nargs: Optional[Literal["+"]]
choices: Optional[Any]
@model_validator(mode="after") # type: ignore
@classmethod
def validate_action(cls, values: "CustomArgument"):
if values.type is bool and values.action != "store_true":
raise ValueError('If type is bool, action must be "store_true"')
return values
@model_validator(mode="after") # type: ignore
@classmethod
def remove_props_on_store_true(cls, values: "CustomArgument"):
if values.action == "store_true":
values.type = None
values.nargs = None
values.choices = None
return values
# override
def model_dump(self, **kwargs):
res = super().model_dump(**kwargs)
# Check if choices is present and if it's an empty tuple remove it
if "choices" in res and not res["choices"]:
del res["choices"]
return res
class CustomArgumentGroup(BaseModel):
name: str
arguments: List[CustomArgument]
class ReferenceToCustomArgumentsProcessor:
def __init__(self, reference: Dict[str, Dict]):
"""Initializes the ReferenceToCustomArgumentsProcessor."""
self.reference = reference
self.custom_groups: Dict[str, List[CustomArgumentGroup]] = {}
self.build_custom_groups()
@staticmethod
def _make_type_parsable(type_: str) -> type:
"""Make the type parsable by removing the annotations."""
if "Union" in type_ and "str" in type_:
return str
if "Union" in type_ and "int" in type_:
return int
if type_ in ["date", "datetime.time", "time"]:
return str
if any(x in type_ for x in ["gt=", "ge=", "lt=", "le="]):
if "Annotated" in type_:
type_ = type_.replace("Annotated[", "").replace("]", "")
type_ = type_.split(",")[0]
return eval(type_) # noqa: S307, E501 pylint: disable=eval-used
def _parse_type(self, type_: str) -> type:
"""Parse the type from the string representation."""
type_ = self._make_type_parsable(type_) # type: ignore
if get_origin(type_) is Literal:
type_ = type(get_args(type_)[0]) # type: ignore
return type_ # type: ignore
def _get_nargs(self, type_: type) -> Optional[Union[int, str]]:
"""Get the nargs for the given type."""
if get_origin(type_) is list:
return "+"
return None
def _get_choices(self, type_: str) -> Tuple:
"""Get the choices for the given type."""
type_ = self._make_type_parsable(type_) # type: ignore
type_origin = get_origin(type_)
choices = ()
if type_origin is Literal:
choices = get_args(type_)
# param_type = type(choices[0])
if type_origin is list:
type_ = get_args(type_)[0]
if get_origin(type_) is Literal:
choices = get_args(type_)
# param_type = type(choices[0])
if type_origin is Union and type(None) in get_args(type_):
# remove NoneType from the args
args = [arg for arg in get_args(type_) if arg != type(None)]
# if there is only one arg left, use it
if len(args) > 1:
raise ValueError("Union with NoneType should have only one type left")
type_ = args[0]
if get_origin(type_) is Literal:
choices = get_args(type_)
# param_type = type(choices[0])
return choices
def build_custom_groups(self):
"""Build the custom groups from the reference."""
for route, v in self.reference.items():
for provider, args in v["parameters"].items():
if provider == "standard":
continue
custom_arguments = []
for arg in args:
if arg.get("standard"):
continue
type_ = self._parse_type(arg["type"])
custom_arguments.append(
CustomArgument(
name=arg["name"],
type=type_,
dest=arg["name"],
default=arg["default"],
required=not (arg["optional"]),
action="store" if type_ != bool else "store_true",
help=arg["description"],
nargs=self._get_nargs(type_), # type: ignore
choices=self._get_choices(arg["type"]),
)
)
group = CustomArgumentGroup(name=provider, arguments=custom_arguments)
if route not in self.custom_groups:
self.custom_groups[route] = []
self.custom_groups[route].append(group)
class ArgparseTranslator:
def __init__(
self,
func: Callable,
custom_argument_groups: Optional[List[CustomArgumentGroup]] = None,
add_help: Optional[bool] = True,
):
"""
Initializes the ArgparseTranslator.
Args:
func (Callable): The function to translate into an argparse program.
add_help (Optional[bool], optional): Whether to add the help argument. Defaults to False.
"""
self.func = func
self.signature = inspect.signature(func)
self.type_hints = get_type_hints(func)
self.provider_parameters = []
self._parser = argparse.ArgumentParser(
prog=func.__name__,
description=self._build_description(func.__doc__), # type: ignore
formatter_class=argparse.RawTextHelpFormatter,
add_help=add_help if add_help else False,
)
self._required = self._parser.add_argument_group("required arguments")
if any(param in self.type_hints for param in self.signature.parameters):
self._generate_argparse_arguments(self.signature.parameters)
if custom_argument_groups:
for group in custom_argument_groups:
argparse_group = self._parser.add_argument_group(group.name)
for argument in group.arguments:
kwargs = argument.model_dump(exclude={"name"}, exclude_none=True)
# If the argument is already in use, we can't repeat it
if f"--{argument.name}" not in self._parser_arguments():
argparse_group.add_argument(f"--{argument.name}", **kwargs)
self.provider_parameters.append(argument.name)
def _parser_arguments(self) -> List[str]:
"""Get all the arguments from all groups currently defined on the parser."""
arguments_in_use: List[str] = []
# pylint: disable=protected-access
for action_group in self._parser._action_groups:
for action in action_group._group_actions:
arguments_in_use.extend(action.option_strings)
return arguments_in_use
@property
def parser(self) -> argparse.ArgumentParser:
return deepcopy(self._parser)
@staticmethod
def _build_description(func_doc: str) -> str:
"""Builds the description of the argparse program from the function docstring."""
patterns = ["openbb\n ======", "Parameters\n ----------"]
if func_doc:
for pattern in patterns:
if pattern in func_doc:
func_doc = func_doc[: func_doc.index(pattern)].strip()
break
return func_doc
@staticmethod
def _param_is_default(param: inspect.Parameter) -> bool:
"""Returns True if the parameter has a default value."""
return param.default != inspect.Parameter.empty
def _get_action_type(self, param: inspect.Parameter) -> str:
"""Returns the argparse action type for the given parameter."""
param_type = self.type_hints[param.name]
if param_type == bool:
return ArgparseActionType.store_true.value
return ArgparseActionType.store.value
def _get_type_and_choices(
self, param: inspect.Parameter
) -> Tuple[Type[Any], Tuple[Any, ...]]:
"""Returns the type and choices for the given parameter."""
param_type = self.type_hints[param.name]
type_origin = get_origin(param_type)
choices = ()
if type_origin is Literal:
choices = get_args(param_type)
param_type = type(choices[0]) # type: ignore
if type_origin is list: # TODO: dict should also go here
param_type = get_args(param_type)[0]
if get_origin(param_type) is Literal:
choices = get_args(param_type)
param_type = type(choices[0]) # type: ignore
if type_origin is Union:
union_args = get_args(param_type)
if str in union_args:
param_type = str
# check if it's an Optional, which would be a Union with NoneType
if type(None) in get_args(param_type):
# remove NoneType from the args
args = [arg for arg in get_args(param_type) if arg != type(None)]
# if there is only one arg left, use it
if len(args) > 1:
raise ValueError(
"Union with NoneType should have only one type left"
)
param_type = args[0]
if get_origin(param_type) is Literal:
choices = get_args(param_type)
param_type = type(choices[0]) # type: ignore
# if there are custom choices, override
choices = self._get_argument_custom_choices(param) or choices # type: ignore
return param_type, choices
@staticmethod
def _split_annotation(
base_annotation: Type[Any], custom_annotation_type: Type
) -> Tuple[Type[Any], List[Any]]:
"""Find the base annotation and the custom annotations, namely the OpenBBField."""
if get_origin(base_annotation) is not Annotated:
return base_annotation, []
base_annotation, *maybe_custom_annotations = get_args(base_annotation)
return base_annotation, [
annotation
for annotation in maybe_custom_annotations
if isinstance(annotation, custom_annotation_type)
]
@classmethod
def _get_argument_custom_help(cls, param: inspect.Parameter) -> Optional[str]:
"""Returns the help annotation for the given parameter."""
base_annotation = param.annotation
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
help_annotation = (
custom_annotations[0].description if custom_annotations else None
)
if not help_annotation:
# try to get it from the docstring
pass
return help_annotation
@classmethod
def _get_argument_custom_choices(cls, param: inspect.Parameter) -> Optional[str]:
"""Returns the help annotation for the given parameter."""
base_annotation = param.annotation
_, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
choices_annotation = (
custom_annotations[0].choices if custom_annotations else None
)
if not choices_annotation:
# try to get it from the docstring
pass
return choices_annotation
def _get_nargs(self, param: inspect.Parameter) -> Optional[str]:
"""Returns the nargs annotation for the given parameter."""
param_type = self.type_hints[param.name]
origin = get_origin(param_type)
if origin is list:
return "+"
if origin is Union and any(
get_origin(arg) is list for arg in get_args(param_type)
):
return "+"
return None
def _generate_argparse_arguments(self, parameters) -> None:
"""Generates the argparse arguments from the function parameters."""
for param in parameters.values():
# TODO : how to handle kwargs?
# it's possible to add unknown arguments when parsing as follows:
# args, unknown_args = parser.parse_known_args()
if param.name == "kwargs":
continue
param_type, choices = self._get_type_and_choices(param)
# if the param is a custom type, we need to flatten it
if inspect.isclass(param_type) and issubclass(param_type, BaseModel):
# update type hints with the custom type fields
type_hints = get_type_hints(param_type)
# prefix the type hints keys with the param name
type_hints = {
f"{param.name}{SEP}{key}": value
for key, value in type_hints.items()
}
self.type_hints.update(type_hints)
# create a signature from the custom type
sig = inspect.signature(param_type)
# add help to the annotation
annotated_parameters: List[inspect.Parameter] = []
for child_param in sig.parameters.values():
new_child_param = child_param.replace(
name=f"{param.name}{SEP}{child_param.name}",
annotation=Annotated[
child_param.annotation,
OpenBBField(
description=param_type.model_json_schema()[
"properties"
][child_param.name].get("description", None)
),
],
kind=inspect.Parameter.KEYWORD_ONLY,
)
annotated_parameters.append(new_child_param)
# replacing with the annotated parameters
new_signature = inspect.Signature(
parameters=annotated_parameters,
return_annotation=sig.return_annotation,
)
self._generate_argparse_arguments(new_signature.parameters)
# the custom type itself should not be added as an argument
continue
required = not self._param_is_default(param)
kwargs = {
"type": param_type,
"dest": param.name,
"default": param.default,
"required": required,
"action": self._get_action_type(param),
"help": self._get_argument_custom_help(param),
"nargs": self._get_nargs(param),
}
if choices:
kwargs["choices"] = choices
if param_type == bool:
# store_true action does not accept the below kwargs
kwargs.pop("type")
kwargs.pop("nargs")
if required:
self._required.add_argument(
f"--{param.name}",
**kwargs,
)
else:
self._parser.add_argument(
f"--{param.name}",
**kwargs,
)
@staticmethod
def _unflatten_args(args: dict) -> Dict[str, Any]:
"""Unflatten the args that were flattened by the custom types."""
result: Dict[str, Any] = {}
for key, value in args.items():
if SEP in key:
parts = key.split(SEP)
nested_dict = result
for part in parts[:-1]:
if part not in nested_dict:
nested_dict[part] = {}
nested_dict = nested_dict[part]
nested_dict[parts[-1]] = value
else:
result[key] = value
return result
def _update_with_custom_types(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""Update the kwargs with the custom types."""
# for each argument in the signature that is a custom type, we need to
# update the kwargs with the custom type kwargs
for param in self.signature.parameters.values():
# TODO : how to handle kwargs?
if param.name == "kwargs":
continue
param_type, _ = self._get_type_and_choices(param)
if inspect.isclass(param_type) and issubclass(param_type, BaseModel):
custom_type_kwargs = kwargs[param.name]
kwargs[param.name] = param_type(**custom_type_kwargs)
return kwargs
def execute_func(
self,
parsed_args: Optional[argparse.Namespace] = None,
) -> Any:
"""
Executes the original function with the parsed arguments.
Args:
parsed_args (Optional[argparse.Namespace], optional): The parsed arguments. Defaults to None.
Returns:
Any: The return value of the original function.
"""
kwargs = self._unflatten_args(vars(parsed_args))
kwargs = self._update_with_custom_types(kwargs)
# remove kwargs that doesn't match the signature or provider parameters
kwargs = {
key: value
for key, value in kwargs.items()
if key in self.signature.parameters or key in self.provider_parameters
}
return self.func(**kwargs)
def parse_args_and_execute(self) -> Any:
"""
Parses the arguments and executes the original function.
Returns:
Any: The return value of the original function.
"""
parsed_args = self._parser.parse_args()
return self.execute_func(parsed_args)
def translate(self) -> Callable:
"""
Wraps the original function with an argparse program.
Returns:
Callable: The original function wrapped with an argparse program.
"""
def wrapper_func():
return self.parse_args_and_execute()
return wrapper_func

View File

@@ -0,0 +1,76 @@
"""Registry for OBBjects."""
import json
from typing import Dict, List
from openbb_core.app.model.abstract.singleton import SingletonMeta
from openbb_core.app.model.obbject import OBBject
class Registry(metaclass=SingletonMeta):
obbjects: List[OBBject] = []
@staticmethod
def _contains_obbject(uuid: str, obbjects: List[OBBject]) -> bool:
"""Check if obbject with uuid is in the registry."""
return any(obbject.id == uuid for obbject in obbjects)
@classmethod
def register(cls, obbject: OBBject):
"""Designed to add an OBBject instance to the registry."""
if isinstance(obbject, OBBject) and not cls._contains_obbject(
obbject.id, cls.obbjects
):
cls.obbjects.append(obbject)
@classmethod
def get(cls, idx: int) -> OBBject:
"""Return the obbject at index idx."""
# the list should work as a stack
# i.e., the last element needs to be accessed by idx=0 and so on
reversed_list = list(reversed(cls.obbjects))
return reversed_list[idx]
@property
def all(self) -> Dict[int, Dict]:
"""Return all obbjects in the registry"""
def _handle_standard_params(obbject: OBBject) -> str:
"""Handle standard params for obbjects"""
standard_params_json = ""
std_params = obbject._standard_params # pylint: disable=protected-access
if hasattr(std_params, "__dict__"):
standard_params = {
k: str(v)[:30] for k, v in std_params.__dict__.items() if v
}
standard_params_json = json.dumps(standard_params)
return standard_params_json
def _handle_data_repr(obbject: OBBject) -> str:
"""Handle data representation for obbjects"""
data_repr = ""
if hasattr(obbject, "results") and obbject.results:
data_schema = (
obbject.results[0].model_json_schema()
if obbject.results and isinstance(obbject.results, list)
else ""
)
if data_schema and "title" in data_schema:
data_repr = f"{data_schema['title']}"
if data_schema and "description" in data_schema:
data_repr += f" - {data_schema['description'].split('.')[0]}"
return data_repr
obbjects = {}
for i, obbject in enumerate(list(reversed(self.obbjects))):
obbjects[i] = {
"route": obbject._route, # pylint: disable=protected-access
"provider": obbject.provider,
"standard params": _handle_standard_params(obbject),
"data": _handle_data_repr(obbject),
}
return obbjects

36
cli/openbb_cli/assets/i18n/en.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
en:
intro: introduction on the OpenBB Platform CLI
about: discover the capabilities of the OpenBB Platform CLI (https://openbb.co/docs)
support: pre-populate a support ticket for our team to evaluate
survey: fill in our 2-minute survey so we better understand how we can improve the CLI
settings: enable and disable feature flags, preferences and settings
_scripts_: Record and execute your own .openbb routine scripts
record: start recording current session
stop: stop session recording and convert to .openbb routine
exe: execute .openbb routine scripts (use exe --example for an example)
_configure_: Configure your own CLI
_main_menu_: Main menu
settings/_info_: Feature flags
settings/_settings_: Settings and preferences
settings/retryload: retry misspelled commands with load first
settings/interactive: open dataframes in interactive window
settings/cls: clear console after each command
settings/color: use coloring features
settings/promptkit: enable prompt toolkit (autocomplete and history)
settings/thoughts: thoughts of the day
settings/reporthtml: open report as HTML otherwise notebook
settings/exithelp: automatically print help when quitting menu
settings/rcontext: remember contexts loaded params during session
settings/rich: colorful rich CLI
settings/richpanel: colorful rich CLI panel
settings/watermark: watermark in figures
settings/cmdloc: command location displayed in figures
settings/overwrite: whether to overwrite Excel files if they already exists
settings/version: whether to show the version in the bottom right corner
settings/tbhint: displays usage hints in the bottom toolbar
settings/console_style: apply a custom rich style to the CLI
settings/flair: choose flair icon
settings/timezone: pick timezone
settings/language: translation language
settings/n_rows: number of rows to show on non interactive tables
settings/n_cols: number of columns to show on non interactive tables

View File

@@ -0,0 +1,21 @@
# Go into the equity context
equity
# Get the company's profile
profile --symbol aapl
# Get company's statements
fundamental
balance --symbol aapl
cash --symbol aapl
transcript --symbol aapl --year 2023
# Load company's historical data
../price
historical --symbol aapl
# Get its performance
performance --symbol aapl
# Export the candle as an image and the historical data into a csv
historical --symbol aapl --chart --export csv,jpg

Binary file not shown.

View File

@@ -0,0 +1,47 @@
{
"style_name": "dark",
"base_mpf_style": null,
"marketcolors": {
"candle": {
"up": "#00ACFF",
"down": "#e4003a"
},
"edge": {
"up": "#00ACFF",
"down": "#e4003a"
},
"wick": {
"up": "#00ACFF",
"down": "#e4003a"
},
"ohlc": {
"up": "#00ACFF",
"down": "#e4003a"
},
"volume": {
"up": "#00ACFF",
"down": "#e4003a"
},
"vcedge": {
"up": "#219E4F",
"down": "#9E1711"
},
"vcdopcod": true,
"alpha": 1
},
"mavcolors": [
"#EB3DBC",
"#31EBEA",
"#EB8C54",
"#EB5549"
],
"y_on_right": true,
"gridcolor": "#A3A0A2",
"gridstyle": ":",
"facecolor": "black",
"edgecolor": null,
"figcolor": null,
"gridaxis": null,
"rc": null,
"legacy_rc": null
}

View File

@@ -0,0 +1,7 @@
{
"xticks_rotation": 10,
"tight_layout_padding": 2,
"pie_wedgeprops": {"linewidth": 0.5, "edgecolor": "#FFFFFF"},
"pie_startangle": 90,
"volume_bar_width": 0.5
}

View File

@@ -0,0 +1,96 @@
# The cool hackerman style
# LINES
# http://matplotlib.org/api/artist_api.html#module-matplotlib.lines
lines.linewidth: 1.5
lines.color: F5EFF3
lines.linestyle: -
lines.marker: None
lines.markeredgewidth: 0.5
lines.markersize: 4
lines.dash_joinstyle: miter
lines.dash_capstyle: butt
lines.solid_joinstyle: miter
lines.solid_capstyle: projecting
lines.antialiased: True
patch.edgecolor: F5EFF3
patch.facecolor: 00aaff
patch.linewidth: 1
# TEXT
# http://matplotlib.org/api/font_manager_api.html
font.family: sans-serif
font.sans-serif: Consolas, Deja Vu Sans, Arial, Helvetica
font.style: normal
font.variant: normal
font.weight: medium
font.stretch: normal
font.size: 16
text.color: F5EFF3
axes.spines.left: true
axes.spines.bottom: true
axes.spines.top: true
axes.spines.right: true
axes.linewidth: 0.4
axes.labelsize: small
axes.titlelocation: left
axes.facecolor: black
axes.edgecolor: F5EFF3
axes.labelcolor: F5EFF3
axes.grid: true
axes.grid.which: major
axes.prop_cycle: cycler('color', ['ffed00', 'ef7d00', 'e4003a', 'c13246', '822661', '48277c', '005ca9', '00aaff', '9b30d9', 'af005f', '5f00af', 'af87ff'])
xtick.color: F5EFF3
ytick.color: F5EFF3
xtick.major.size: 2
xtick.minor.size: 1
xtick.labelsize: small
xtick.alignment: center
xtick.major.width: 0.2
ytick.major.size: 2
ytick.minor.size: 1
ytick.labelsize: small
ytick.left: False
ytick.labelleft: False
ytick.right: True
ytick.labelright: True
ytick.major.width: 0.2
grid.color: F5EFF3
grid.linewidth: 0.4
grid.linestyle: :
legend.framealpha: 0.6
legend.frameon: true
legend.facecolor: black
legend.edgecolor: black
legend.scatterpoints: 3
legend.fontsize: small
legend.loc: upper left
figure.facecolor: black
figure.edgecolor: black
figure.subplot.hspace: 0.2
savefig.facecolor: black
savefig.edgecolor: black
### Boxplots
boxplot.boxprops.color: F5EFF3
boxplot.capprops.color: F5EFF3
boxplot.flierprops.color: F5EFF3
boxplot.flierprops.markeredgecolor: F5EFF3
boxplot.whiskerprops.color: F5EFF3
boxplot.medianprops.color: F5EFF3
boxplot.meanprops.markeredgecolor: F5EFF3
boxplot.meanprops.color: F5EFF3

View File

@@ -0,0 +1,132 @@
{
"line": {
"up_color": "#00ACFF",
"down_color": "#e4003a",
"color": "#ffed00",
"width": 1.5
},
"data": {
"candlestick": [
{
"decreasing": {
"fillcolor": "#e4003a",
"line": {
"color": "#e4003a"
}
},
"increasing": {
"fillcolor": "#00ACFF",
"line": {
"color": "#00ACFF"
}
},
"type": "candlestick"
}
]
},
"layout": {
"annotationdefaults": {
"showarrow": false
},
"autotypenumbers": "strict",
"colorway": [
"#ffed00",
"#ef7d00",
"#e4003a",
"#c13246",
"#822661",
"#48277c",
"#005ca9",
"#00aaff",
"#9b30d9",
"#af005f",
"#5f00af",
"#af87ff"
],
"dragmode": "pan",
"font": {
"family": "Fira Code",
"size": 18
},
"hoverlabel": {
"align": "left"
},
"mapbox": {
"style": "dark"
},
"hovermode": "x",
"legend": {
"bgcolor": "rgba(0, 0, 0, 0.5)",
"x": 0.01,
"xanchor": "left",
"y": 0.99,
"yanchor": "top",
"font": {
"size": 15
}
},
"legend2": {
"bgcolor": "rgba(0, 0, 0, 0.5)",
"font": {
"size": 15
}
},
"legend3": {
"bgcolor": "rgba(0, 0, 0, 0.5)",
"font": {
"size": 15
}
},
"legend4": {
"bgcolor": "rgba(0, 0, 0, 0.5)",
"font": {
"size": 15
}
},
"legend5": {
"bgcolor": "rgba(0, 0, 0, 0.5)",
"font": {
"size": 15
}
},
"paper_bgcolor": "#000000",
"plot_bgcolor": "#000000",
"xaxis": {
"automargin": true,
"autorange": true,
"rangeslider": {
"visible": false
},
"showgrid": true,
"showline": true,
"tickfont": {
"size": 14
},
"zeroline": false,
"tick0": 1,
"title": {
"standoff": 20
},
"linecolor": "#F5EFF3",
"mirror": true,
"ticks": "outside"
},
"yaxis": {
"anchor": "x",
"automargin": true,
"fixedrange": false,
"zeroline": false,
"showgrid": true,
"showline": true,
"side": "right",
"tick0": 0.5,
"title": {
"standoff": 20
},
"gridcolor": "#283442",
"linecolor": "#F5EFF3",
"mirror": true,
"ticks": "outside"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"info": "rgb(224,131,48)",
"cmds": "rgb(102,203,228)",
"param": "rgb(247,206,70)",
"menu": "rgb(50,115,185)",
"src": "rgb(216,90,64)",
"unvl": "grey30",
"help": "#FAC900"
}

View File

@@ -0,0 +1,47 @@
{
"style_name": "light",
"base_mpf_style": null,
"marketcolors": {
"candle": {
"up": "#00ACFF",
"down": "#e4003a"
},
"edge": {
"up": "#00ACFF",
"down": "#e4003a"
},
"wick": {
"up": "#00ACFF",
"down": "#e4003a"
},
"ohlc": {
"up": "#00ACFF",
"down": "#e4003a"
},
"volume": {
"up": "#00ACFF",
"down": "#e4003a"
},
"vcedge": {
"up": "#219E4F",
"down": "#9E1711"
},
"vcdopcod": true,
"alpha": 1
},
"mavcolors": [
"#EB3DBC",
"#31EBEA",
"#EB8C54",
"#EB5549"
],
"y_on_right": true,
"gridcolor": "grey",
"gridstyle": ":",
"facecolor": "white",
"edgecolor": null,
"figcolor": null,
"gridaxis": null,
"rc": null,
"legacy_rc": null
}

View File

@@ -0,0 +1,7 @@
{
"xticks_rotation": 10,
"tight_layout_padding": 2,
"pie_wedgeprops": {"linewidth": 0.5, "edgecolor": "#000000"},
"pie_startangle": 90,
"volume_bar_width": 0.5
}

View File

@@ -0,0 +1,95 @@
# The cool hackerman style
# LINES
# http://matplotlib.org/api/artist_api.html#module-matplotlib.lines
lines.linewidth: 1.1
lines.color: 0F0F0F
lines.linestyle: -
lines.marker: None
lines.markeredgewidth: 0.5
lines.markersize: 4
lines.dash_joinstyle: miter
lines.dash_capstyle: butt
lines.solid_joinstyle: miter
lines.solid_capstyle: projecting
lines.antialiased: True
patch.edgecolor: 000000
patch.facecolor: 822661
patch.linewidth: 1
# TEXT
# http://matplotlib.org/api/font_manager_api.html
font.family: sans-serif
font.sans-serif: Consolas, Deja Vu Sans, Arial, Helvetica
font.style: normal
font.variant: normal
font.weight: medium
font.stretch: normal
font.size: 13
text.color: 0F0F0F
axes.spines.left: true
axes.spines.bottom: true
axes.spines.top: true
axes.spines.right: true
axes.linewidth: 0.2
axes.labelsize: small
axes.titlelocation: left
axes.facecolor: white
axes.edgecolor: 0F0F0F
axes.labelcolor: 0F0F0F
axes.grid: true
axes.grid.which: major
axes.prop_cycle: cycler('color', ['254495', 'c13246', '48277c', 'e4003a', 'ef7d00', '822661', 'ffed00', '00aaff', '9b30d9', 'af005f', '5f00af', 'af87ff'])
xtick.color: 0F0F0F
ytick.color: 0F0F0F
xtick.major.size: 2
xtick.minor.size: 1
xtick.labelsize: small
ytick.major.size: 2
ytick.minor.size: 1
ytick.labelsize: small
xtick.alignment: center
ytick.left: False
ytick.labelleft: False
ytick.right: True
ytick.labelright: True
grid.color: 0F0F0F
grid.linewidth: 0.4
grid.linestyle: :
legend.framealpha: 0.6
legend.frameon: true
legend.facecolor: white
legend.edgecolor: white
legend.scatterpoints: 3
legend.fontsize: small
legend.loc: best
figure.facecolor: white
figure.edgecolor: white
figure.subplot.hspace: 0.2
savefig.facecolor: white
savefig.edgecolor: white
### Boxplots
boxplot.boxprops.color: 0F0F0F
boxplot.capprops.color: 0F0F0F
boxplot.flierprops.color: 0F0F0F
boxplot.flierprops.markeredgecolor: 0F0F0F
boxplot.whiskerprops.color: 0F0F0F
boxplot.medianprops.color: 0F0F0F
boxplot.meanprops.markeredgecolor: 0F0F0F
boxplot.meanprops.color: 0F0F0F

View File

@@ -0,0 +1,871 @@
{
"line": {
"up_color": "#009600",
"down_color": "#c80000",
"color": "#0d0887",
"width": 1.5,
"down_color_transparent": "rgba(200, 0, 0, 0.4)",
"up_color_transparent": "rgba(0, 150, 0, 0.4)"
},
"data": {
"barpolar": [
{
"marker": {
"line": {
"color": "white",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "white",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "#C8D4E3",
"linecolor": "#C8D4E3",
"minorgridcolor": "#C8D4E3",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "#C8D4E3",
"linecolor": "#C8D4E3",
"minorgridcolor": "#C8D4E3",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"heatmapgl": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmapgl"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
],
"candlestick": [
{
"decreasing": {
"fillcolor": "#c80000",
"line": {
"color": "#990000"
}
},
"increasing": {
"fillcolor": "#009600",
"line": {
"color": "#007500"
}
},
"type": "candlestick"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1,
"showarrow": false
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#254495",
"#c13246",
"#48277c",
"#e4003a",
"#ef7d00",
"#822661",
"#ffed00",
"#00aaff",
"#9b30d9",
"#af005f",
"#5f00af",
"#af87ff"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "white",
"showlakes": true,
"showland": true,
"subunitcolor": "#C8D4E3"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "x",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "white",
"polar": {
"angularaxis": {
"gridcolor": "#EBF0F8",
"linecolor": "#EBF0F8",
"ticks": ""
},
"bgcolor": "white",
"radialaxis": {
"gridcolor": "#EBF0F8",
"linecolor": "#EBF0F8",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "white",
"gridcolor": "#DFE8F3",
"gridwidth": 2,
"linecolor": "#EBF0F8",
"showbackground": true,
"ticks": "",
"zerolinecolor": "#EBF0F8"
},
"yaxis": {
"backgroundcolor": "white",
"gridcolor": "#DFE8F3",
"gridwidth": 2,
"linecolor": "#EBF0F8",
"showbackground": true,
"ticks": "",
"zerolinecolor": "#EBF0F8"
},
"zaxis": {
"backgroundcolor": "white",
"gridcolor": "#DFE8F3",
"gridwidth": 2,
"linecolor": "#EBF0F8",
"showbackground": true,
"ticks": "",
"zerolinecolor": "#EBF0F8"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "#DFE8F3",
"linecolor": "#A2B1C6",
"ticks": ""
},
"baxis": {
"gridcolor": "#DFE8F3",
"linecolor": "#A2B1C6",
"ticks": ""
},
"bgcolor": "white",
"caxis": {
"gridcolor": "#DFE8F3",
"linecolor": "#A2B1C6",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"ticks": "",
"zerolinewidth": 2,
"rangeslider": {
"visible": false
},
"showgrid": true,
"showline": true,
"tickfont": {
"size": 15
},
"mirror": true,
"zeroline": false
},
"yaxis": {
"automargin": true,
"ticks": "",
"tickfont": {
"size": 15
},
"zerolinewidth": 2,
"fixedrange": false,
"showgrid": true,
"showline": true,
"side": "right",
"mirror": true,
"zeroline": false
},
"dragmode": "pan",
"legend": {
"bgcolor": "rgba(0, 0, 0, 0)",
"x": 1.1,
"xanchor": "left",
"y": 0.99,
"yanchor": "top"
},
"legend2": {
"bgcolor": "rgba(0, 0, 0, 0)"
},
"legend3": {
"bgcolor": "rgba(0, 0, 0, 0)"
},
"legend4": {
"bgcolor": "rgba(0, 0, 0, 0)"
},
"legend5": {
"bgcolor": "rgba(0, 0, 0, 0)"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"info": "rgb(224,131,48)",
"cmds": "rgb(70,156,222)",
"param": "rgb(247,206,70)",
"menu": "rgb(50,115,185)",
"src": "rgb(216,90,64)",
"unvl": "grey30",
"help": "#FAC900"
}

View File

@@ -0,0 +1,102 @@
{
"data": {
"candlestick": [
{
"decreasing": {
"fillcolor": "#e4003a",
"line": {
"color": "#e4003a"
}
},
"increasing": {
"fillcolor": "#00ACFF",
"line": {
"color": "#00ACFF"
}
},
"type": "candlestick"
}
]
},
"layout": {
"annotationdefaults": {
"showarrow": false
},
"autotypenumbers": "strict",
"colorway": [
"#ffed00",
"#ef7d00",
"#e4003a",
"#c13246",
"#822661",
"#48277c",
"#005ca9",
"#00aaff",
"#9b30d9",
"#af005f",
"#5f00af",
"#af87ff"
],
"dragmode": "pan",
"font": {
"family": "Fira Code",
"size": 18
},
"hoverlabel": {
"align": "left"
},
"mapbox": {
"style": "light"
},
"hovermode": "x",
"legend": {
"bgcolor": "rgba(0, 0, 0, 0)",
"x": 0.01,
"xanchor": "left",
"y": 0.99,
"yanchor": "top",
"font": {
"size": 15
}
},
"paper_bgcolor": "white",
"plot_bgcolor": "white",
"xaxis": {
"automargin": true,
"autorange": true,
"rangeslider": {
"visible": false
},
"showgrid": true,
"showline": true,
"tickfont": {
"size": 14
},
"zeroline": false,
"tick0": 1,
"title": {
"standoff": 20
},
"linecolor": "#F5EFF3",
"mirror": true,
"ticks": "outside"
},
"yaxis": {
"anchor": "x",
"automargin": true,
"fixedrange": false,
"zeroline": false,
"showgrid": true,
"showline": true,
"side": "right",
"tick0": 0.5,
"title": {
"standoff": 20
},
"gridcolor": "#283442",
"linecolor": "#F5EFF3",
"mirror": true,
"ticks": "outside"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"info": "rgb(224,131,48)",
"cmds": "#2A7C6E",
"param": "rgb(247,206,70)",
"menu": "#427A2E",
"src": "rgb(216,90,64)",
"unvl": "grey30",
"help": "#FAC900"
}

20
cli/openbb_cli/cli.py Normal file
View File

@@ -0,0 +1,20 @@
"""OpenBB Platform CLI entry point."""
import sys
from openbb_cli.config.setup import bootstrap
from openbb_cli.controllers.cli_controller import launch
def main():
"""Use the main entry point for the OpenBB CLI."""
bootstrap()
dev = "--dev" in sys.argv[1:]
debug = "--debug" in sys.argv[1:]
launch(dev, debug)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
"""Core config init."""

View File

@@ -0,0 +1,403 @@
"""Nested completer for completion of OpenBB hierarchical data structures."""
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Mapping,
Optional,
Pattern,
Set,
Union,
)
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import AnyFormattedText
NestedDict = Mapping[str, Union[Any, Set[str], None, Completer]]
# pylint: disable=too-many-arguments,global-statement,too-many-branches,global-variable-not-assigned
class WordCompleter(Completer):
"""Simple autocompletion on a list of words.
:param words: List of words or callable that returns a list of words.
:param ignore_case: If True, case-insensitive completion.
:param meta_dict: Optional dict mapping words to their meta-text. (This
should map strings to strings or formatted text.)
:param WORD: When True, use WORD characters.
:param sentence: When True, don't complete by comparing the word before the
cursor, but by comparing all the text before the cursor. In this case,
the list of words is just a list of strings, where each string can
contain spaces. (Can not be used together with the WORD option.)
:param match_middle: When True, match not only the start, but also in the
middle of the word.
:param pattern: Optional compiled regex for finding the word before
the cursor to complete. When given, use this regex pattern instead of
default one (see document._FIND_WORD_RE)
"""
def __init__(
self,
words: Union[List[str], Callable[[], List[str]]],
ignore_case: bool = False,
display_dict: Optional[Mapping[str, AnyFormattedText]] = None,
meta_dict: Optional[Mapping[str, AnyFormattedText]] = None,
WORD: bool = True,
sentence: bool = False,
match_middle: bool = False,
pattern: Optional[Pattern[str]] = None,
) -> None:
"""Initialize the WordCompleter."""
assert not (WORD and sentence) # noqa: S101
self.words = words
self.ignore_case = ignore_case
self.display_dict = display_dict or {}
self.meta_dict = meta_dict or {}
self.WORD = WORD
self.sentence = sentence
self.match_middle = match_middle
self.pattern = pattern
def get_completions(
self,
document: Document,
_complete_event: CompleteEvent,
) -> Iterable[Completion]:
"""Get completions."""
# Get list of words.
words = self.words
if callable(words):
words = words()
# Get word/text before cursor.
if self.sentence:
word_before_cursor = document.text_before_cursor
else:
word_before_cursor = document.get_word_before_cursor(
WORD=self.WORD, pattern=self.pattern
)
if (
"--" in document.text_before_cursor
and document.text_before_cursor.rfind(" --")
>= document.text_before_cursor.rfind(" -")
):
word_before_cursor = f'--{document.text_before_cursor.split("--")[-1]}'
elif f"--{word_before_cursor}" == document.text_before_cursor:
word_before_cursor = document.text_before_cursor
if self.ignore_case:
word_before_cursor = word_before_cursor.lower()
def word_matches(word: str) -> bool:
"""Set True when the word before the cursor matches."""
if self.ignore_case:
word = word.lower()
if self.match_middle:
return word_before_cursor in word
return word.startswith(word_before_cursor)
for a in words:
if word_matches(a):
display = self.display_dict.get(a, a)
display_meta = self.meta_dict.get(a, "")
yield Completion(
text=a,
start_position=-len(word_before_cursor),
display=display,
display_meta=display_meta,
)
class NestedCompleter(Completer):
"""Completer which wraps around several other completers, and calls any the
one that corresponds with the first word of the input.
By combining multiple `NestedCompleter` instances, we can achieve multiple
hierarchical levels of autocompletion. This is useful when `WordCompleter`
is not sufficient.
If you need multiple levels, check out the `from_nested_dict` classmethod.
"""
complementary: List = list()
def __init__(
self, options: Dict[str, Optional[Completer]], ignore_case: bool = True
) -> None:
"""Initialize the NestedCompleter."""
self.flags_processed: List = list()
self.original_options = options
self.options = options
self.ignore_case = ignore_case
self.complementary = list()
def __repr__(self) -> str:
"""Return string representation of NestedCompleter."""
return f"NestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
@classmethod
def from_nested_dict(cls, data: dict) -> "NestedCompleter":
"""Create a `NestedCompleter`.
It starts from a nested dictionary data structure, like this:
.. code::
data = {
'show': {
'version': None,
'interfaces': None,
'clock': None,
'ip': {'interface': {'brief'}}
},
'exit': None
'enable': None
}
The value should be `None` if there is no further completion at some
point. If all values in the dictionary are None, it is also possible to
use a set instead.
Values in this data structure can be a completers as well.
"""
options: Dict[str, Any] = {}
for key, value in data.items():
if isinstance(value, Completer):
options[key] = value
elif isinstance(value, dict):
options[key] = cls.from_nested_dict(value)
elif isinstance(value, set):
options[key] = cls.from_nested_dict({item: None for item in value})
elif isinstance(key, str) and isinstance(value, str):
options[key] = options[value]
else:
assert value is None # noqa: S101
options[key] = None
for items in cls.complementary:
if items[0] in options:
options[items[1]] = options[items[0]]
elif items[1] in options:
options[items[0]] = options[items[1]]
return cls(options)
def get_completions( # noqa: PLR0912
self, document: Document, complete_event: CompleteEvent
) -> Iterable[Completion]:
"""Get completions."""
# Split document.
cmd = ""
text = document.text_before_cursor.lstrip()
if " " in text:
cmd = text.split(" ")[0]
if "-" in text:
if text.rfind("--") == -1 or text.rfind("-") - 1 > text.rfind("--"):
unprocessed_text = "-" + text.split("-")[-1]
else:
unprocessed_text = "--" + text.split("--")[-1]
else:
unprocessed_text = text
stripped_len = len(document.text_before_cursor) - len(text)
# Check if there are multiple flags for the same command
if self.complementary:
for same_flags in self.complementary:
if (
same_flags[0] in self.flags_processed
and same_flags[1] not in self.flags_processed
) or (
same_flags[1] in self.flags_processed
and same_flags[0] not in self.flags_processed
):
if same_flags[0] in self.flags_processed:
self.flags_processed.append(same_flags[1])
elif same_flags[1] in self.flags_processed:
self.flags_processed.append(same_flags[0])
if cmd:
self.options = {
k: self.original_options.get(cmd).options[k] # type: ignore
for k in self.original_options.get(cmd).options # type: ignore
if k not in self.flags_processed
}
else:
self.options = {
k: self.original_options[k]
for k in self.original_options
if k not in self.flags_processed
}
# If there is a space, check for the first term, and use a subcompleter.
if " " in unprocessed_text:
first_term = unprocessed_text.split()[0]
# user is updating one of the values
if unprocessed_text[-1] != " ":
self.flags_processed = [
flag for flag in self.flags_processed if flag != first_term
]
if self.complementary:
for same_flags in self.complementary:
if (
same_flags[0] in self.flags_processed
and same_flags[1] not in self.flags_processed
) or (
same_flags[1] in self.flags_processed
and same_flags[0] not in self.flags_processed
):
if same_flags[0] in self.flags_processed:
self.flags_processed.remove(same_flags[0])
elif same_flags[1] in self.flags_processed:
self.flags_processed.remove(same_flags[1])
if cmd and self.original_options.get(cmd):
self.options = self.original_options
else:
self.options = {
k: self.original_options[k]
for k in self.original_options
if k not in self.flags_processed
}
if "-" not in text:
completer = self.options.get(first_term)
elif cmd in self.options and self.options.get(cmd):
completer = self.options.get(cmd).options.get(first_term) # type: ignore
else:
completer = self.options.get(first_term)
# If we have a sub completer, use this for the completions.
if completer is not None:
remaining_text = unprocessed_text[len(first_term) :].lstrip()
move_cursor = len(text) - len(remaining_text) + stripped_len
new_document = Document(
remaining_text,
cursor_position=document.cursor_position - move_cursor,
)
# Provides auto-completion but if user doesn't take it still keep going
if " " in new_document.text:
if (
new_document.text in [f"{opt} " for opt in self.options]
or unprocessed_text[-1] == " "
):
self.flags_processed.append(first_term)
if cmd:
self.options = {
k: self.original_options.get(cmd).options[k] # type: ignore
for k in self.original_options.get(cmd).options # type: ignore
if k not in self.flags_processed
}
else:
self.options = {
k: self.original_options[k]
for k in self.original_options
if k not in self.flags_processed
}
# In case the users inputs a single boolean flag
elif not completer.options: # type: ignore
self.flags_processed.append(first_term)
if self.complementary:
for same_flags in self.complementary:
if (
same_flags[0] in self.flags_processed
and same_flags[1] not in self.flags_processed
) or (
same_flags[1] in self.flags_processed
and same_flags[0] not in self.flags_processed
):
if same_flags[0] in self.flags_processed:
self.flags_processed.append(same_flags[1])
elif same_flags[1] in self.flags_processed:
self.flags_processed.append(same_flags[0])
if cmd:
self.options = {
k: self.original_options.get(cmd).options[k] # type: ignore
for k in self.original_options.get(cmd).options # type: ignore
if k not in self.flags_processed
}
else:
self.options = {
k: self.original_options[k]
for k in self.original_options
if k not in self.flags_processed
}
else:
# This is a NestedCompleter
yield from completer.get_completions(new_document, complete_event)
# No space in the input: behave exactly like `WordCompleter`.
else:
# check if the prompt has been updated in the meantime
if " " in text or "-" in text:
actual_flags_processed = [
flag for flag in self.flags_processed if flag in text
]
if self.complementary:
for same_flags in self.complementary:
if (
same_flags[0] in actual_flags_processed
and same_flags[1] not in actual_flags_processed
) or (
same_flags[1] in actual_flags_processed
and same_flags[0] not in actual_flags_processed
):
if same_flags[0] in actual_flags_processed:
actual_flags_processed.append(same_flags[1])
elif same_flags[1] in actual_flags_processed:
actual_flags_processed.append(same_flags[0])
if len(actual_flags_processed) < len(self.flags_processed):
self.flags_processed = actual_flags_processed
if cmd:
self.options = {
k: self.original_options.get(cmd).options[k] # type: ignore
for k in self.original_options.get(cmd).options # type: ignore
if k not in self.flags_processed
}
else:
self.options = {
k: self.original_options[k]
for k in self.original_options
if k not in self.flags_processed
}
command = self.options.get(cmd)
options = command.options if command else {} # type: ignore
command_options = [f"{cmd} {opt}" for opt in options]
text_list = [text in val for val in command_options]
if cmd and cmd in self.options and text_list:
completer = WordCompleter(
list(self.options.get(cmd).options.keys()), # type: ignore
ignore_case=self.ignore_case,
)
elif bool([val for val in self.options if text in val]):
completer = WordCompleter(
list(self.options.keys()), ignore_case=self.ignore_case
)
else:
# The user has delete part of the first command and we need to reset options
if bool([val for val in self.original_options if text in val]):
self.options = self.original_options
self.flags_processed = list()
completer = WordCompleter(
list(self.options.keys()), ignore_case=self.ignore_case
)
# This is a WordCompleter
yield from completer.get_completions(document, complete_event)

View File

@@ -0,0 +1,97 @@
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
from rich import panel
from rich.console import (
Console as RichConsole,
Theme,
)
from rich.text import Text
from openbb_cli.config.menu_text import RICH_TAGS
if TYPE_CHECKING:
from openbb_cli.models.settings import Settings
class Console:
"""Create a rich console to wrap the console print with a Panel."""
def __init__(
self,
settings: "Settings",
style: Optional[Dict[str, Any]] = None,
):
"""Initialize the ConsoleAndPanel class."""
self._console = RichConsole(
theme=Theme(style),
highlight=False,
soft_wrap=True,
)
self._settings = settings
self.menu_text = ""
self.menu_path = ""
def capture(self):
"""Capture the console output."""
return self._console.capture()
@staticmethod
def filter_rich_tags(text):
"""Filter out rich tags from 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):
"""Print the text to the console."""
if kwargs and "text" in list(kwargs) and "menu" in list(kwargs):
if not self._settings.TEST_MODE:
if self._settings.ENABLE_RICH_PANEL:
if self._settings.SHOW_VERSION:
version = self._settings.VERSION
version = f"[param]OpenBB Platform CLI v{version}[/param] (https://openbb.co)"
else:
version = (
"[param]OpenBB Platform CLI[/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"])) # noqa: T201
elif not self._settings.TEST_MODE:
self._console.print(*args, **kwargs)
else:
print(*args, **kwargs) # noqa: T201
def input(self, *args, **kwargs):
"""Get input from the user."""
self.print(*args, **kwargs, end="")
return input()

View File

@@ -0,0 +1,83 @@
"""Constants module."""
from pathlib import Path
# Paths
HOME_DIRECTORY = Path.home()
REPOSITORY_DIRECTORY = Path(__file__).parent.parent.parent.parent
SRC_DIRECTORY = Path(__file__).parent.parent
SETTINGS_DIRECTORY = HOME_DIRECTORY / ".openbb_platform"
ASSETS_DIRECTORY = SRC_DIRECTORY / "assets"
STYLES_DIRECTORY = ASSETS_DIRECTORY / "styles"
ENV_FILE_REPOSITORY = REPOSITORY_DIRECTORY / ".env"
ENV_FILE_PROJECT = REPOSITORY_DIRECTORY / "openbb_cli" / ".env"
ENV_FILE_SETTINGS = SETTINGS_DIRECTORY / ".cli.env"
HIST_FILE_PROMPT = SETTINGS_DIRECTORY / ".cli.his"
I18N_FILE = ASSETS_DIRECTORY / "i18n"
DEFAULT_ROUTINES_URL = "https://openbb-cms.directus.app/items/Routines"
TIMEOUT = 30
CONNECTION_ERROR_MSG = "[red]Connection error.[/red]"
CONNECTION_TIMEOUT_MSG = "[red]Connection timeout.[/red]"
SCRIPT_TAGS = [
"stocks",
"crypto",
"etf",
"economy",
"forex",
"fixed income",
"alternative",
"funds",
"bonds",
"macro",
"mutual funds",
"equities",
"options",
"dark pool",
"shorts",
"insider",
"behavioral analysis",
"fundamental analysis",
"technical analysis",
"quantitative analysis",
"forecasting",
"government",
"comparison",
"nft",
"on chain",
"off chain",
"screener",
"report",
"overview",
"rates",
"econometrics",
"portfolio",
"real estate",
]
AVAILABLE_FLAIRS = {
":openbb": "(🦋)",
":bug": "(🐛)",
":rocket": "(🚀)",
":diamond": "(💎)",
":stars": "(✨)",
":baseball": "(⚾)",
":boat": "(⛵)",
":phone": "(☎)",
":mercury": "(☿)",
":hidden": "",
":sun": "(☼)",
":moon": "(☾)",
":nuke": "(☢)",
":hazard": "(☣)",
":tunder": "(☈)",
":king": "(♔)",
":queen": "(♕)",
":knight": "(♘)",
":recycle": "(♻)",
":scales": "(⚖)",
":ball": "(⚽)",
":golf": "(⛳)",
":piece": "(☮)",
":yy": "(☯)",
}

View File

@@ -0,0 +1,262 @@
"""Rich Module."""
__docformat__ = "numpy"
from typing import Dict, List, Optional, Union
import i18n
from openbb import obb
# https://rich.readthedocs.io/en/stable/appendix/colors.html#appendix-colors
# https://rich.readthedocs.io/en/latest/highlighting.html#custom-highlighters
RICH_TAGS = [
"[menu]",
"[/menu]",
"[cmds]",
"[/cmds]",
"[info]",
"[/info]",
"[param]",
"[/param]",
"[src]",
"[/src]",
"[help]",
"[/help]",
]
USE_COLOR = True
def get_ordered_list_sources(command_path: str) -> List:
"""Return the preferred source for the given command.
Parameters
----------
command_path: str
The command to find the source for. E.g. "/equity/price/historical
Returns
-------
List
The list of sources for the given command.
"""
command_reference = obb.reference.get("paths", {}).get(command_path, {}) # type: ignore
if command_reference:
providers = list(command_reference["parameters"].keys())
return [provider for provider in providers if provider != "standard"]
return []
class MenuText:
"""Create menu text with rich colors to be displayed by CLI."""
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
self.warnings: List[Dict[str, str]] = []
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)
space = (
(col_align - len(parameter_translated)) * " "
if col_align > len(parameter_translated)
else ""
)
self.menu_text += f"[param]{parameter_translated}{space}:[/param] {value}\n"
def _adjust_command_length(self, key_command: str) -> str:
"""Adjust the length of the command if it is too long.
Parameters
----------
key_command : str
command to be adjusted
Returns
-------
str
adjusted command
"""
if len(key_command) > 18:
new_key_command = key_command[:18] # Default to trimming to 18 characters
if "_" in key_command:
key_command_split = key_command.split("_")
new_key_command = (
"_".join(key_command_split[:2])
if len(key_command_split) > 2
else key_command_split[0]
)
if len(new_key_command) > 18:
new_key_command = new_key_command[:18]
if new_key_command != key_command:
self.warnings.append(
{
"warning": "Command name too long",
"command": key_command,
"trimmed_command": new_key_command,
}
)
key_command = new_key_command
return key_command
def _handle_command_description(
self, key_command: str, command_description: str
) -> str:
"""Handle the command description.
Parameters
----------
key_command : str
command to be adjusted
command_description : str
description of the command
Returns
-------
str
adjusted command description
"""
if not command_description:
command_description = i18n.t(self.menu_path + key_command)
if command_description == self.menu_path + key_command:
command_description = ""
return (
command_description[:88] + "..."
if len(command_description) > 91
else command_description
)
def add_cmd(
self, key_command: str, condition: bool = True, command_description: str = ""
):
"""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.
"""
key_command = self._adjust_command_length(key_command)
command_description = self._handle_command_description(
key_command, command_description
)
spacing = (23 - (len(key_command) + 4)) * " "
cmd = f"{key_command}{spacing}{command_description}"
cmd = f"[cmds] {cmd}[/cmds]" if condition else f"[unvl] {cmd}[/unvl]"
sources = get_ordered_list_sources(f"{self.menu_path}{key_command}")
if sources:
space = (self.col_src - len(cmd)) * " " if self.col_src > len(cmd) else " "
cmd += f"{space}[src][{', '.join(sources)}][/src]"
self.menu_text += cmd + "\n"
def add_menu(
self,
key_menu: str,
condition: Optional[Union[bool, str]] = True,
menu_description: str = "",
):
"""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 menu_description:
menu = f"{key_menu}{spacing}{menu_description}"
else:
menu_description = i18n.t(self.menu_path + key_menu)
if menu_description == self.menu_path + key_menu:
menu_description = ""
menu = f"{key_menu}{spacing}{menu_description}"
if condition:
self.menu_text += f"[menu]> {menu}[/menu]\n"
else:
self.menu_text += f"[unvl]> {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"

View File

@@ -0,0 +1,85 @@
"""Configuration for the CLI."""
import copy
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, TypeVar
import i18n
from openbb_cli.config.constants import ENV_FILE_SETTINGS, I18N_FILE, SETTINGS_DIRECTORY
from openbb_cli.session import Session
if TYPE_CHECKING:
from openbb_charting.core.openbb_figure import OpenBBFigure
# ruff: noqa:PLW0603
OpenBBFigureT = TypeVar("OpenBBFigureT", bound="OpenBBFigure")
HOLD: bool = False
COMMAND_ON_CHART: bool = True
current_figure: Optional[OpenBBFigureT] = None # type: ignore
new_axis: bool = True
legends: List = []
last_legend = ""
# pylint: disable=global-statement
def set_last_legend(leg: str):
"""Set the last legend."""
global last_legend
last_legend = copy.deepcopy(leg)
def reset_legend() -> None:
"""Reset the legend."""
global legends
legends = []
def get_legends() -> list:
"""Get the legends."""
return legends
def set_same_axis() -> None:
"""Set the same axis."""
global new_axis
new_axis = False
def set_new_axis() -> None:
"""Set the new axis."""
global new_axis
new_axis = True
def make_new_axis() -> bool:
"""Make a new axis."""
return new_axis
def get_current_figure() -> Optional["OpenBBFigure"]:
"""Get the current figure."""
return current_figure
def set_current_figure(fig: Optional[OpenBBFigureT] = None):
"""Set the current figure."""
# pylint: disable=global-statement
global current_figure
current_figure = fig
def setup_i18n(i18n_path: Path = I18N_FILE, lang: str = "en"):
"""Select the CLI translation language."""
i18n.load_path.append(i18n_path)
i18n.set("locale", lang)
i18n.set("filename_format", "{locale}.{format}")
def bootstrap():
"""Setup pre-launch configurations for the CLI."""
SETTINGS_DIRECTORY.mkdir(parents=True, exist_ok=True)
Path(ENV_FILE_SETTINGS).touch(exist_ok=True)
setup_i18n(lang=Session().settings.USE_LANGUAGE)

View File

@@ -0,0 +1,108 @@
"""Chart and style helpers for Plotly."""
# pylint: disable=C0302,R0902,W3301
import json
from pathlib import Path
from typing import Any, Dict, List, Optional
from rich.console import Console
from openbb_cli.config.constants import STYLES_DIRECTORY
console = Console()
class Style:
"""The class that helps with handling of style configurations.
It serves styles for 2 libraries. For `Plotly` this class serves absolute paths
to the .pltstyle files. For `Plotly` and `Rich` this class serves custom
styles as python dictionaries.
"""
STYLES_REPO = STYLES_DIRECTORY
console_styles_available: Dict[str, Path] = {}
console_style: Dict[str, Any] = {}
line_color: str = ""
up_color: str = ""
down_color: str = ""
up_colorway: List[str] = []
down_colorway: List[str] = []
up_color_transparent: str = ""
down_color_transparent: str = ""
line_width: float = 1.5
def __init__(
self,
style: Optional[str] = "",
directory: Optional[Path] = None,
):
"""Initialize the class."""
self._load(directory)
self.apply(style, directory)
def apply(
self, style: Optional[str] = None, directory: Optional[Path] = None
) -> None:
"""Apply the style to the console."""
if style:
if style in self.console_styles_available:
json_path: Optional[Path] = self.console_styles_available[style]
else:
self._load(directory)
if style in self.console_styles_available:
json_path = self.console_styles_available[style]
else:
console.print(f"\nInvalid console style '{style}', using default.")
json_path = self.console_styles_available.get("dark", None)
if json_path:
self.console_style = self._from_json(json_path)
else:
console.print("Error loading default.")
def _from_directory(self, folder: Optional[Path]) -> None:
"""Load custom styles from folder.
Parses the styles/default and styles/user folders and loads style files.
To be recognized files need to follow a naming convention:
*.pltstyle - plotly stylesheets
*.richstyle.json - rich stylesheets
Parameters
----------
folder : str
Path to the folder containing the stylesheets
"""
if not folder or not folder.exists():
return
for attr, ext in zip(
["console_styles_available"],
[".richstyle.json"],
):
for file in folder.rglob(f"*{ext}"):
getattr(self, attr)[file.name.replace(ext, "")] = file
def _load(self, directory: Optional[Path] = None) -> None:
"""Load custom styles from default and user folders."""
self._from_directory(self.STYLES_REPO)
self._from_directory(directory)
def _from_json(self, file: Path) -> Dict[str, Any]:
"""Load style from json file."""
with open(file) as f:
json_style: dict = json.load(f)
for key, value in json_style.items():
json_style[key] = value.replace(
" ", ""
) # remove whitespaces so Rich can parse it
return json_style
@property
def available_styles(self) -> List[str]:
"""Return available styles."""
return list(self.console_styles_available.keys())

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,291 @@
"""Platform Equity Controller."""
import os
from functools import partial, update_wrapper
from types import MethodType
from typing import Dict, List, Optional
import pandas as pd
from openbb import obb
from openbb_charting.core.openbb_figure import OpenBBFigure
from openbb_cli.argparse_translator.argparse_class_processor import (
ArgparseClassProcessor,
)
from openbb_cli.argparse_translator.obbject_registry import Registry
from openbb_cli.config.completer import NestedCompleter
from openbb_cli.config.menu_text import MenuText
from openbb_cli.controllers.base_controller import BaseController
from openbb_cli.controllers.utils import export_data, print_rich_table
from openbb_cli.session import Session
class DummyTranslation:
"""Dummy Translation for testing."""
def __init__(self):
"""Construct a Dummy Translation Class."""
self.paths = {}
self.translators = {}
class PlatformController(BaseController):
"""Platform Controller Base class."""
CHOICES_GENERATION = True
def __init__(
self,
name: str,
parent_path: List[str],
platform_target: Optional[type] = None,
queue: Optional[List[str]] = None,
translators: Optional[Dict] = None,
):
"""Construct a Platform based Controller."""
self.PATH = f"/{'/'.join(parent_path)}/{name}/" if parent_path else f"/{name}/"
super().__init__(queue)
self._name = name
if not (platform_target or translators):
raise ValueError("Either platform_target or translators must be provided.")
self._translated_target = (
ArgparseClassProcessor(
target_class=platform_target, reference=obb.reference["paths"] # type: ignore
)
if platform_target
else DummyTranslation()
)
self.translators = (
translators
if translators is not None
else getattr(self._translated_target, "translators", {})
)
self.paths = getattr(self._translated_target, "paths", {})
if self.translators:
self._link_obbject_to_data_processing_commands()
self._generate_commands()
self._generate_sub_controllers()
if Session().prompt_session and Session().settings.USE_PROMPT_TOOLKIT:
choices: dict = self.choices_default
self.completer = NestedCompleter.from_nested_dict(choices)
def _link_obbject_to_data_processing_commands(self):
"""Link data processing commands to OBBject registry."""
for _, trl in self.translators.items():
for action in trl._parser._actions: # pylint: disable=protected-access
if action.dest == "data":
action.choices = range(len(Registry.obbjects))
action.type = int
action.nargs = None
def _intersect_data_processing_commands(self, ns_parser):
"""Intersect data processing commands and change the obbject id into an actual obbject."""
if hasattr(ns_parser, "data") and ns_parser.data in range(
len(Registry.obbjects)
):
obbject = Registry.get(ns_parser.data)
setattr(ns_parser, "data", obbject.results)
return ns_parser
def _generate_sub_controllers(self):
"""Handle paths."""
for path, value in self.paths.items():
if value == "path":
continue
sub_menu_translators = {}
choices_commands = []
for translator_name, translator in self.translators.items():
if f"{self._name}_{path}" in translator_name:
new_name = translator_name.replace(f"{self._name}_{path}_", "")
sub_menu_translators[new_name] = translator
choices_commands.append(new_name)
if translator_name in self.CHOICES_COMMANDS:
self.CHOICES_COMMANDS.remove(translator_name)
# Create the sub controller as a new class
class_name = f"{self._name.capitalize()}{path.capitalize()}Controller"
SubController = type(
class_name,
(PlatformController,),
{
"CHOICES_GENERATION": True,
# "CHOICES_MENUS": [],
"CHOICES_COMMANDS": choices_commands,
},
)
self._generate_controller_call(
controller=SubController,
name=path,
parent_path=self.path,
translators=sub_menu_translators,
)
def _generate_commands(self):
"""Generate commands."""
for name, translator in self.translators.items():
# Prepare the translator name to create a command call in the controller
new_name = name.replace(f"{self._name}_", "")
self._generate_command_call(name=new_name, translator=translator)
def _generate_command_call(self, name, translator):
"""Generate command call."""
def method(self, other_args: List[str], translator=translator):
"""Call the translator."""
parser = translator.parser
if ns_parser := self.parse_known_args_and_warn(
parser=parser,
other_args=other_args,
export_allowed="raw_data_and_figures",
):
try:
ns_parser = self._intersect_data_processing_commands(ns_parser)
obbject = translator.execute_func(parsed_args=ns_parser)
df: pd.DataFrame = None
fig: OpenBBFigure = None
if obbject:
Registry.register(obbject)
if hasattr(ns_parser, "chart") and ns_parser.chart:
obbject.show()
fig = obbject.chart.fig
if hasattr(obbject, "to_dataframe"):
df = obbject.to_dataframe()
elif isinstance(obbject, dict):
df = pd.DataFrame.from_dict(obbject, orient="index")
else:
df = None
elif hasattr(obbject, "to_dataframe"):
df = obbject.to_dataframe()
if isinstance(df.columns, pd.RangeIndex):
df.columns = [str(i) for i in df.columns]
print_rich_table(df, show_index=True)
elif isinstance(obbject, dict):
df = pd.DataFrame.from_dict(obbject, orient="index")
print_rich_table(df, show_index=True)
elif obbject:
Session().console.print(obbject)
if hasattr(ns_parser, "export") and ns_parser.export:
sheet_name = getattr(ns_parser, "sheet_name", None)
export_data(
export_type=ns_parser.export,
dir_path=os.path.dirname(os.path.abspath(__file__)),
func_name=translator.func.__name__,
df=df,
sheet_name=sheet_name,
figure=fig,
)
except Exception as e:
Session().console.print(f"[red]{e}[/]\n")
return
# Bind the method to the class
bound_method = MethodType(method, self)
# Update the wrapper and set the attribute
bound_method = update_wrapper( # type: ignore
partial(bound_method, translator=translator), method
)
setattr(self, f"call_{name}", bound_method)
def _generate_controller_call(self, controller, name, parent_path, translators):
"""Generate controller call."""
def method(self, _, controller, name, parent_path, translators):
"""Call the controller."""
self.queue = self.load_class(
class_ins=controller,
name=name,
parent_path=parent_path,
translators=translators,
queue=self.queue,
)
# Bind the method to the class
bound_method = MethodType(method, self)
# Update the wrapper and set the attribute
bound_method = update_wrapper( # type: ignore
partial(
bound_method,
name=name,
parent_path=parent_path,
translators=translators,
controller=controller,
),
method,
)
setattr(self, f"call_{name}", bound_method)
def _get_command_description(self, command: str) -> str:
"""Get command description."""
command_description = (
obb.reference["paths"] # type: ignore
.get(f"{self.PATH}{command}", {})
.get("description", "")
)
if not command_description:
trl = self.translators.get(
f"{self._name}_{command}"
) or self.translators.get(command)
if trl and hasattr(trl, "parser"):
command_description = trl.parser.description
return command_description.split(".")[0].lower()
def _get_menu_description(self, menu: str) -> str:
"""Get menu description."""
menu_description = (
obb.reference["routers"] # type: ignore
.get(f"{self.PATH}{menu}", {})
.get("description", "")
) or ""
return menu_description.split(".")[0].lower()
def print_help(self):
"""Print help."""
mt = MenuText(self.PATH)
if self.CHOICES_MENUS:
for menu in self.CHOICES_MENUS:
menu_description = self._get_menu_description(menu)
mt.add_menu(key_menu=menu, menu_description=menu_description)
if self.CHOICES_COMMANDS:
mt.add_raw("\n")
for command in self.CHOICES_COMMANDS:
command_description = self._get_command_description(command)
mt.add_cmd(
key_command=command.replace(f"{self._name}_", ""),
command_description=command_description,
)
Session().console.print(text=mt.menu_text, menu=self._name)
settings = Session().settings
dev_mode = settings.DEBUG_MODE or settings.TEST_MODE
if mt.warnings and dev_mode:
Session().console.print("")
for w in mt.warnings:
w_str = str(w).replace("{", "").replace("}", "").replace("'", "")
Session().console.print(f"[yellow]{w_str}[/yellow]")
Session().console.print("")

View File

@@ -0,0 +1,322 @@
"""This module contains functions to build the choice map for the controllers."""
from argparse import SUPPRESS, ArgumentParser
from contextlib import contextmanager
from inspect import isfunction, unwrap
from types import MethodType
from typing import Callable, List, Literal
from unittest.mock import patch
from openbb_cli.controllers.utils import (
check_file_type_saved,
check_positive,
)
from openbb_cli.session import Session
def __mock_parse_known_args_and_warn(
controller, # pylint: disable=unused-argument
parser: ArgumentParser,
other_args: List[str],
export_allowed: Literal[
"no_export", "raw_data_only", "figures_only", "raw_data_and_figures"
] = "no_export",
raw: bool = False,
limit: int = 0,
) -> None:
"""Add arguments.
Add the arguments that would have normally added by :
- openbb_cli.base_controller.BaseController.parse_known_args_and_warn
Parameters
----------
parser: argparse.ArgumentParser
Parser with predefined arguments
other_args: List[str]
list of arguments to parse
export_allowed: Literal["no_export", "raw_data_only", "figures_only", "raw_data_and_figures"]
Export options
raw: bool
Add the --raw flag
limit: int
Add a --limit flag with this number default
"""
_ = other_args
parser.add_argument(
"-h", "--help", action="store_true", help="show this help message"
)
if export_allowed != "no_export":
choices_export = []
help_export = "Does not export!"
if export_allowed == "raw_data_only":
choices_export = ["csv", "json", "xlsx"]
help_export = "Export raw data into csv, json, xlsx"
elif export_allowed == "figures_only":
choices_export = ["png", "jpg", "pdf", "svg"]
help_export = "Export figure into png, jpg, pdf, svg "
else:
choices_export = ["csv", "json", "xlsx", "png", "jpg", "pdf", "svg"]
help_export = "Export raw data into csv, json, xlsx and figure into png, jpg, pdf, svg "
parser.add_argument(
"--export",
default="",
type=check_file_type_saved(choices_export),
dest="export",
help=help_export,
choices=choices_export,
)
if raw:
parser.add_argument(
"--raw",
dest="raw",
action="store_true",
default=False,
help="Flag to display raw data",
)
if limit > 0:
parser.add_argument(
"-l",
"--limit",
dest="limit",
default=limit,
help="Number of entries to show in data.",
type=check_positive,
)
def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> None:
"""Add arguments.
Add the arguments that would have normally added by:
- openbb_cli.parent_classes.BaseController.parse_simple_args
Parameters
----------
parser: argparse.ArgumentParser
Parser with predefined arguments
other_args: List[str]
List of arguments to parse
"""
parser.add_argument(
"-h", "--help", action="store_true", help="show this help message"
)
_ = other_args
def __get_command_func(controller, command: str):
"""Get the function with the name `f"call_{command}"` from controller object.
Parameters
----------
controller: BaseController
Instance of the CLI Controller.
command: str
A name from controller.CHOICES_COMMANDS
Returns
-------
Callable: Command function.
"""
if command not in controller.CHOICES_COMMANDS:
raise AttributeError(
f"The following command is not inside `CHOICES_COMMANDS` : '{command}'"
)
command = f"call_{command}"
command_func = getattr(controller, command)
command_func = unwrap(func=command_func)
if isfunction(command_func):
command_func = MethodType(command_func, controller)
return command_func
def contains_functions_to_patch(command_func: Callable) -> bool:
"""Check command function.
Check if a `command_func` actually contains the functions we want to mock, i.e.:
- parse_simple_args
- parse_known_args_and_warn
Parameters
----------
command_func: Callable
Function to check.
Returns
-------
bool: Whether or not `command_func` contains the mocked functions.
"""
co_names = command_func.__code__.co_names
return bool(
"parse_simple_args" in co_names or "parse_known_args_and_warn" in co_names
)
@contextmanager
def __patch_controller_functions(controller):
"""Patch controller functions.
Patch the following function from a BaseController instance:
- parse_simple_args
- parse_known_args_and_warn
These functions take an 'argparse.ArgumentParser' object as parameter.
We want to intercept this 'argparse.ArgumentParser' object.
Parameters
----------
controller: BaseController
BaseController object that needs to be patched.
Returns
-------
List[Callable]: List of mocked functions.
"""
bound_mock_parse_known_args_and_warn = MethodType(
__mock_parse_known_args_and_warn,
controller,
)
rich = patch(
target="openbb_cli.config.console.Console.print",
return_value=None,
)
patcher_list = [
patch.object(
target=controller,
attribute="parse_simple_args",
side_effect=__mock_parse_simple_args,
return_value=None,
),
patch.object(
target=controller,
attribute="parse_known_args_and_warn",
side_effect=bound_mock_parse_known_args_and_warn,
return_value=None,
),
]
if not Session().settings.DEBUG_MODE:
rich.start()
patched_function_list = []
for patcher in patcher_list:
patched_function_list.append(patcher.start())
yield patched_function_list
if not Session().settings.DEBUG_MODE:
rich.stop()
for patcher in patcher_list:
patcher.stop()
def _get_argument_parser(
controller,
command: str,
) -> ArgumentParser:
"""Intercept the ArgumentParser instance from the command function.
A command function being a function starting with `call_`, like:
- call_help
- call_overview
- call_load
Parameters
----------
controller: BaseController
Instance of the CLI Controller.
command: str
A name from `controller.CHOICES_COMMANDS`.
Returns
-------
ArgumentParser: ArgumentParser instance from the command function.
"""
command_func: Callable = __get_command_func(controller=controller, command=command)
if not contains_functions_to_patch(command_func=command_func):
raise AssertionError(
f"One of these functions should be inside `call_{command}`:\n"
" - parse_simple_args\n"
" - parse_known_args_and_warn\n"
)
with __patch_controller_functions(controller=controller) as patched_function_list:
command_func([])
call_count = 0
for patched_function in patched_function_list:
call_count += patched_function.call_count
if patched_function.call_count == 1:
args, kwargs = patched_function.call_args
argument_parser = (
kwargs["parser"] if kwargs.get("parser", None) else args[0]
)
if call_count != 1:
raise AssertionError(
f"One of these functions should be called once inside `call_{command}`:\n"
" - parse_simple_args\n"
" - parse_known_args_and_warn\n"
)
return argument_parser
def _build_command_choice_map(argument_parser: ArgumentParser) -> dict:
"""Build the choice map for a command."""
choice_map: dict = {}
for action in argument_parser._actions: # pylint: disable=protected-access
if action.help == SUPPRESS:
continue
if len(action.option_strings) == 1:
long_name = action.option_strings[0]
short_name = ""
elif len(action.option_strings) == 2:
short_name = action.option_strings[0]
long_name = action.option_strings[1]
else:
raise AttributeError(f"Invalid argument_parser: {argument_parser}")
if hasattr(action, "choices") and action.choices:
choice_map[long_name] = {str(c): {} for c in action.choices}
else:
choice_map[long_name] = {}
if short_name and long_name:
choice_map[short_name] = long_name
return choice_map
def build_controller_choice_map(controller) -> dict:
"""Build the choice map for a controller."""
command_list = controller.CHOICES_COMMANDS
controller_choice_map: dict = {c: {} for c in controller.controller_choices}
for command in command_list:
try:
argument_parser = _get_argument_parser(
controller=controller,
command=command,
)
controller_choice_map[command] = _build_command_choice_map(
argument_parser=argument_parser
)
except Exception as exception:
if Session().settings.DEBUG_MODE:
raise Exception(
f"On command : `{command}`.\n{str(exception)}"
) from exception
return controller_choice_map

View File

@@ -0,0 +1,947 @@
#!/usr/bin/env python
"""Main CLI Module."""
import argparse
import contextlib
import difflib
import logging
import os
import re
import sys
import time
import webbrowser
from datetime import datetime
from functools import partial, update_wrapper
from pathlib import Path
from types import MethodType
from typing import Any, Dict, List, Optional
import certifi
import pandas as pd
import requests
from openbb import obb
from openbb_cli.argparse_translator.obbject_registry import Registry
from openbb_cli.config import constants
from openbb_cli.config.completer import NestedCompleter
from openbb_cli.config.constants import (
ASSETS_DIRECTORY,
ENV_FILE_SETTINGS,
HOME_DIRECTORY,
REPOSITORY_DIRECTORY,
)
from openbb_cli.config.menu_text import MenuText
from openbb_cli.controllers.base_controller import BaseController
from openbb_cli.controllers.platform_controller_factory import (
PlatformControllerFactory,
)
from openbb_cli.controllers.script_parser import is_reset, parse_openbb_script
from openbb_cli.controllers.utils import (
bootup,
first_time_user,
get_flair_and_username,
is_installer,
parse_and_split_input,
print_goodbye,
print_rich_table,
reset,
suppress_stdout,
welcome_message,
)
from openbb_cli.session import Session
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.styles import Style
from pydantic import BaseModel
PLATFORM_ROUTERS = {
d: "menu" if not isinstance(getattr(obb, d), BaseModel) else "command"
for d in dir(obb)
if "_" not in d
}
NON_DATA_ROUTERS = ["coverage", "account", "reference", "system", "user"]
DATA_PROCESSING_ROUTERS = ["technical", "quantitative", "econometrics"]
# pylint: disable=too-many-public-methods,import-outside-toplevel, too-many-function-args
# pylint: disable=too-many-branches,no-member,C0302,too-many-return-statements, inconsistent-return-statements
logger = logging.getLogger(__name__)
env_file = str(ENV_FILE_SETTINGS)
if is_installer():
# Necessary for installer so that it can locate the correct certificates for
# API calls and https
# https://stackoverflow.com/questions/27835619/urllib-and-ssl-certificate-verify-failed-error/73270162#73270162
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
os.environ["SSL_CERT_FILE"] = certifi.where()
class CLIController(BaseController):
"""CLI Controller class."""
CHOICES_COMMANDS = ["record", "stop", "exe", "results"]
CHOICES_MENUS = [
"settings",
]
for router, value in PLATFORM_ROUTERS.items():
if value == "menu":
CHOICES_MENUS.append(router)
else:
CHOICES_COMMANDS.append(router)
PATH = "/"
CHOICES_GENERATION = False
def __init__(self, jobs_cmds: Optional[List[str]] = None):
"""Construct CLI controller."""
self.ROUTINE_FILES: Dict[str, str] = dict()
self.ROUTINE_DEFAULT_FILES: Dict[str, str] = dict()
self.ROUTINE_PERSONAL_FILES: Dict[str, str] = dict()
self.ROUTINE_CHOICES: Dict[str, Any] = dict()
super().__init__(jobs_cmds)
self.queue: List[str] = list()
if jobs_cmds:
self.queue = parse_and_split_input(
an_input=" ".join(jobs_cmds), custom_filters=[]
)
self.update_success = False
self._generate_platform_commands()
self.update_runtime_choices()
def _generate_platform_commands(self):
"""Generate Platform based commands/menus."""
def method_call_class(self, _, controller, name, parent_path, target):
self.queue = self.load_class(
controller, name, parent_path, target, self.queue
)
# pylint: disable=unused-argument
def method_call_command(self, _, router: str):
"""Call command."""
mdl = getattr(obb, router)
df = pd.DataFrame.from_dict(mdl.model_dump(), orient="index")
return print_rich_table(df, show_index=True)
for router, value in PLATFORM_ROUTERS.items():
target = getattr(obb, router)
if value == "menu":
pcf = PlatformControllerFactory(
target, reference=obb.reference["paths"] # type: ignore
)
DynamicController = pcf.create()
# Bind the method to the class
bound_method = MethodType(method_call_class, self)
# Update the wrapper and set the attribute
bound_method = update_wrapper( # type: ignore
partial(
bound_method,
controller=DynamicController,
name=router,
target=target,
parent_path=self.path,
),
method_call_class,
)
else:
bound_method = MethodType(method_call_command, self)
bound_method = update_wrapper( # type: ignore
partial(bound_method, router=router),
method_call_command,
)
setattr(self, f"call_{router}", bound_method)
def update_runtime_choices(self):
"""Update runtime choices."""
routines_directory = Path(
Session().user.preferences.export_directory, "routines"
)
if Session().prompt_session and Session().settings.USE_PROMPT_TOOLKIT:
# choices: dict = self.choices_default
choices: dict = {c: {} for c in self.controller_choices} # type: ignore
choices["hold"] = {c: None for c in ["on", "off", "-s", "--sameaxis"]}
choices["hold"]["off"] = {"--title": None}
self.ROUTINE_FILES = {
filepath.name: filepath # type: ignore
for filepath in routines_directory.rglob("*.openbb")
}
self.ROUTINE_DEFAULT_FILES = {
filepath.name: filepath # type: ignore
for filepath in Path(routines_directory / "hub" / "default").rglob(
"*.openbb"
)
}
self.ROUTINE_PERSONAL_FILES = {
filepath.name: filepath # type: ignore
for filepath in Path(routines_directory / "hub" / "personal").rglob(
"*.openbb"
)
}
choices["exe"] = {
"--file": {
filename: {} for filename in list(self.ROUTINE_FILES.keys())
},
"-f": "--file",
"--example": None,
"-e": "--example",
"--input": None,
"-i": "--input",
"--url": None,
}
choices["record"] = {
"--name": None,
"-n": "--name",
"--description": None,
"-d": "--description",
"--public": None,
"-p": "--public",
"--local": None,
"-l": "--local",
"--tag1": {c: None for c in constants.SCRIPT_TAGS},
"--tag2": {c: None for c in constants.SCRIPT_TAGS},
"--tag3": {c: None for c in constants.SCRIPT_TAGS},
}
self.completer = NestedCompleter.from_nested_dict(choices)
def print_help(self):
"""Print help."""
mt = MenuText("")
mt.add_info("_configure_")
mt.add_menu("settings")
mt.add_raw("\n")
mt.add_info("_scripts_")
mt.add_cmd("record")
mt.add_cmd("stop")
mt.add_cmd("exe")
mt.add_raw("\n")
mt.add_info("Platform CLI")
mt.add_raw(" data\n")
for router, value in PLATFORM_ROUTERS.items():
if router in NON_DATA_ROUTERS or router in DATA_PROCESSING_ROUTERS:
continue
if value == "menu":
menu_description = (
obb.reference["routers"] # type: ignore
.get(f"{self.PATH}{router}", {})
.get("description")
) or ""
mt.add_menu(
key_menu=router,
menu_description=menu_description.split(".")[0].lower(),
)
else:
mt.add_cmd(router)
if any(router in PLATFORM_ROUTERS for router in DATA_PROCESSING_ROUTERS):
mt.add_raw("\n data processing\n")
for router, value in PLATFORM_ROUTERS.items():
if router not in DATA_PROCESSING_ROUTERS:
continue
if value == "menu":
menu_description = (
obb.reference["routers"] # type: ignore
.get(f"{self.PATH}{router}", {})
.get("description")
) or ""
mt.add_menu(
key_menu=router,
menu_description=menu_description.split(".")[0].lower(),
)
else:
mt.add_cmd(router)
mt.add_raw("\n configuration\n")
for router, value in PLATFORM_ROUTERS.items():
if router not in NON_DATA_ROUTERS or router == "reference":
continue
if value == "menu":
menu_description = (
obb.reference["routers"] # type: ignore
.get(f"{self.PATH}{router}", {})
.get("description")
) or ""
mt.add_menu(
key_menu=router,
menu_description=menu_description.split(".")[0].lower(),
)
else:
mt.add_cmd(router)
mt.add_raw("\n cached results (OBBjects)\n")
mt.add_cmd("results")
Session().console.print(text=mt.menu_text, menu="Home")
self.update_runtime_choices()
def parse_input(self, an_input: str) -> List:
"""Overwrite the BaseController parse_input for `askobb` and 'exe'.
This will allow us to search for something like "P/E" ratio.
"""
# Filtering out sorting parameters with forward slashes like P/E
sort_filter = r"((\ -q |\ --question|\ ).*?(/))"
# Filter out urls
url = r"(exe (--url )?(https?://)?my\.openbb\.(dev|co)/u/.*/routine/.*)"
custom_filters = [sort_filter, url]
return parse_and_split_input(an_input=an_input, custom_filters=custom_filters)
def call_settings(self, _):
"""Process feature flags command."""
from openbb_cli.controllers.feature_flags_controller import (
FeatureFlagsController,
)
self.queue = self.load_class(FeatureFlagsController, self.queue)
def call_exe(self, other_args: List[str]):
"""Process exe command."""
# Merge rest of string path to other_args and remove queue since it is a dir
other_args += self.queue
if not other_args:
Session().console.print(
"[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[/info]"
)
return
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="exe",
description="Execute automated routine script. For an example, please use "
"`exe --example` and for documentation and to learn how create your own script "
"type `about exe`.",
)
parser.add_argument(
"--file",
"-f",
help="The path or .openbb file to run.",
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
and "--url" not in other_args
and "my.openbb" not in other_args[0],
type=str,
nargs="+",
)
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.add_argument(
"-e",
"--example",
help="Run an example script to understand how routines can be used.",
dest="example",
action="store_true",
default=False,
)
parser.add_argument(
"--url", help="URL to run openbb script from.", dest="url", type=str
)
if other_args and "-" not in other_args[0][0]:
if other_args[0].startswith("my.") or other_args[0].startswith("http"):
other_args.insert(0, "--url")
else:
other_args.insert(0, "--file")
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser:
if ns_parser.example:
routine_path = ASSETS_DIRECTORY / "routines" / "routine_example.openbb"
Session().console.print(
"[info]Executing an example, please type `about exe` "
"to learn how to create your own script.[/info]\n"
)
time.sleep(3)
elif ns_parser.url:
if not ns_parser.url.startswith(
"https"
) and not ns_parser.url.startswith("http:"):
url = "https://" + ns_parser.url
elif ns_parser.url.startswith("http://"):
url = ns_parser.url.replace("http://", "https://")
else:
url = ns_parser.url
username = url.split("/")[-3]
script_name = url.split("/")[-1]
file_name = f"{username}_{script_name}.openbb"
final_url = f"{url}?raw=true"
response = requests.get(final_url, timeout=10)
if response.status_code != 200:
Session().console.print(
"[red]Could not find the requested script.[/red]"
)
return
routine_text = response.json()["script"]
file_path = Path(
Session().user.preferences.export_directory, "routines"
)
routine_path = file_path / file_name
with open(routine_path, "w") as file:
file.write(routine_text)
self.update_runtime_choices()
elif ns_parser.file:
file_path = " ".join(ns_parser.file) # type: ignore
# if string is not in this format "default/file.openbb" then check for files in ROUTINE_FILES
full_path = file_path
hub_routine = file_path.split("/") # type: ignore
# Change with: my.openbb.co
if hub_routine[0] == "default":
routine_path = Path(
self.ROUTINE_DEFAULT_FILES.get(hub_routine[1], full_path)
)
elif hub_routine[0] == "personal":
routine_path = Path(
self.ROUTINE_PERSONAL_FILES.get(hub_routine[1], full_path)
)
else:
routine_path = Path(self.ROUTINE_FILES.get(file_path, full_path)) # type: ignore
else:
return
with open(routine_path) as fp:
raw_lines = list(fp)
# Capture ARGV either as list if args separated by commas or as single value
if ns_parser.routine_args:
script_inputs = (
ns_parser.routine_args
if "," not in ns_parser.routine_args
else ns_parser.routine_args.split(",")
)
err, parsed_script = parse_openbb_script(
raw_lines=raw_lines,
script_inputs=script_inputs if ns_parser.routine_args else None,
)
# If there err output is not an empty string then it means there was an
# issue in parsing the routine and therefore we don't want to feed it
# to the terminal
if err:
Session().console.print(err)
return
self.queue = [
val
for val in parse_and_split_input(
an_input=parsed_script, custom_filters=[]
)
if val
]
if "export" in self.queue[0]:
export_path = self.queue[0].split(" ")[1]
# If the path selected does not start from the user root, give relative location from root
if export_path[0] == "~":
export_path = export_path.replace(
"~", HOME_DIRECTORY.as_posix()
)
elif export_path[0] != "/":
export_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), export_path
)
# Check if the directory exists
if os.path.isdir(export_path):
Session().console.print(
f"Export data to be saved in the selected folder: '{export_path}'"
)
else:
os.makedirs(export_path)
Session().console.print(
f"[green]Folder '{export_path}' successfully created.[/green]"
)
self.queue = self.queue[1:]
def call_results(self, _):
"""Process results command."""
results = Registry().all
if results:
df = pd.DataFrame.from_dict(results, orient="index")
print_rich_table(
df, show_index=True, index_name="stack index", title="OBBject Results"
)
else:
Session().console.print("[info]No results found.[/info]")
def handle_job_cmds(jobs_cmds: Optional[List[str]]) -> Optional[List[str]]:
"""Handle job commands."""
# If the path selected does not start from the user root,
# give relative location from root
if jobs_cmds is not None and jobs_cmds:
logger.info("INPUT: %s", "/".join(jobs_cmds))
export_path = ""
if jobs_cmds and "export" in jobs_cmds[0]:
commands = jobs_cmds[0].split("/")
first_split = commands[0].split(" ")
if len(first_split) > 1:
export_path = first_split[1]
jobs_cmds = ["/".join(commands[1:])]
if not export_path:
return jobs_cmds
if export_path[0] == "~":
export_path = export_path.replace("~", HOME_DIRECTORY.as_posix())
elif export_path[0] != "/":
export_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), export_path
)
# Check if the directory exists
if os.path.isdir(export_path):
Session().console.print(
f"Export data to be saved in the selected folder: '{export_path}'"
)
else:
os.makedirs(export_path)
Session().console.print(
f"[green]Folder '{export_path}' successfully created.[/green]"
)
return jobs_cmds
# pylint: disable=unused-argument
def run_cli(jobs_cmds: Optional[List[str]] = None, test_mode=False):
"""Run the CLI menu."""
ret_code = 1
t_controller = CLIController(jobs_cmds)
an_input = ""
jobs_cmds = handle_job_cmds(jobs_cmds)
bootup()
if not jobs_cmds:
welcome_message()
if first_time_user():
with contextlib.suppress(EOFError):
webbrowser.open(
"https://docs.openbb.co/terminal/usage/overview/structure-and-navigation"
)
t_controller.print_help()
while ret_code:
# There is a command in the queue
if t_controller.queue and len(t_controller.queue) > 0:
# If the command is quitting the menu we want to return in here
if t_controller.queue[0] in ("q", "..", "quit"):
print_goodbye()
break
# Consume 1 element from the queue
an_input = t_controller.queue[0]
t_controller.queue = t_controller.queue[1:]
# Print the current location because this was an instruction and we want user to know what was the action
if an_input and an_input.split(" ")[0] in t_controller.CHOICES_COMMANDS:
Session().console.print(f"{get_flair_and_username()} / $ {an_input}")
# Get input command from user
else:
try:
# Get input from user using auto-completion
if Session().prompt_session and Session().settings.USE_PROMPT_TOOLKIT:
# Check if toolbar hint was enabled
if Session().settings.TOOLBAR_HINT:
an_input = Session().prompt_session.prompt( # type: ignore[union-attr]
f"{get_flair_and_username()} / $ ",
completer=t_controller.completer,
search_ignore_case=True,
bottom_toolbar=HTML(
'<style bg="ansiblack" fg="ansiwhite">[h]</style> help menu '
'<style bg="ansiblack" fg="ansiwhite">[q]</style> return to previous menu '
'<style bg="ansiblack" fg="ansiwhite">[e]</style> exit the program '
'<style bg="ansiblack" fg="ansiwhite">[cmd -h]</style> '
"see usage and available options "
'<style bg="ansiblack" fg="ansiwhite">[about (cmd/menu)]</style> '
),
style=Style.from_dict(
{
"bottom-toolbar": "#ffffff bg:#333333",
}
),
)
else:
an_input = Session().prompt_session.prompt( # type: ignore[union-attr]
f"{get_flair_and_username()} / $ ",
completer=t_controller.completer,
search_ignore_case=True,
)
# Get input from user without auto-completion
else:
an_input = input(f"{get_flair_and_username()} / $ ")
except (KeyboardInterrupt, EOFError):
print_goodbye()
break
try:
# Process the input command
t_controller.queue = t_controller.switch(an_input)
if an_input in ("q", "quit", "..", "exit", "e"):
print_goodbye()
break
# Check if the user wants to reset application
if an_input in ("r", "reset") or t_controller.update_success:
reset(t_controller.queue if t_controller.queue else [])
break
except SystemExit:
logger.exception(
"The command '%s' doesn't exist on the / menu.",
an_input,
)
Session().console.print(
f"[red]The command '{an_input}' doesn't exist on the / menu.[/red]\n",
)
similar_cmd = difflib.get_close_matches(
an_input.split(" ")[0] if " " in an_input else an_input,
t_controller.controller_choices,
n=1,
cutoff=0.7,
)
if similar_cmd:
an_input = similar_cmd[0]
if " " in an_input:
candidate_input = (
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
)
if candidate_input == an_input:
an_input = ""
t_controller.queue = []
Session().console.print("\n")
continue
an_input = candidate_input
Session().console.print(f"[green]Replacing by '{an_input}'.[/green]")
t_controller.queue.insert(0, an_input)
def insert_start_slash(cmds: List[str]) -> List[str]:
"""Insert a slash at the beginning of a command sequence."""
if not cmds[0].startswith("/"):
cmds[0] = f"/{cmds[0]}"
if cmds[0].startswith("/home"):
cmds[0] = f"/{cmds[0][5:]}"
return cmds
def run_scripts(
path: Path,
test_mode: bool = False,
verbose: bool = False,
routines_args: Optional[List[str]] = None,
special_arguments: Optional[Dict[str, str]] = None,
output: bool = True,
):
"""Run given .openbb scripts.
Parameters
----------
path : str
The location of the .openbb file
test_mode : bool
Whether the CLI is in test mode
verbose : bool
Whether to run tests in verbose mode
routines_args : List[str]
One or multiple inputs to be replaced in the routine and separated by commas.
E.g. GME,AMC,BTC-USD
special_arguments: Optional[Dict[str, str]]
Replace `${key=default}` with `value` for every key in the dictionary
output: bool
Whether to log tests to txt files
"""
if not path.exists():
Session().console.print(f"File '{path}' doesn't exist. Launching base CLI.\n")
if not test_mode:
run_cli()
# THIS NEEDS TO BE REFACTORED!!! - ITS USED FOR TESTING
with path.open() as fp:
raw_lines = [x for x in fp if (not is_reset(x)) and ("#" not in x) and x]
raw_lines = [
raw_line.strip("\n") for raw_line in raw_lines if raw_line.strip("\n")
]
if routines_args:
lines = []
for rawline in raw_lines:
templine = rawline
for i, arg in enumerate(routines_args):
templine = templine.replace(f"$ARGV[{i}]", arg)
lines.append(templine)
# Handle new testing arguments:
elif special_arguments:
lines = []
for line in raw_lines:
new_line = re.sub(
r"\${[^{]+=[^{]+}",
lambda x: replace_dynamic(x, special_arguments), # type: ignore
line,
)
lines.append(new_line)
else:
lines = raw_lines
if test_mode and "exit" not in lines[-1]:
lines.append("exit")
# Deals with the export with a path with "/" in it
export_folder = ""
if "export" in lines[0]:
export_folder = lines[0].split("export ")[1].rstrip()
lines = lines[1:]
simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}"
file_cmds = simulate_argv.replace("//", "/home/").split()
file_cmds = insert_start_slash(file_cmds) if file_cmds else file_cmds
file_cmds = (
[f"export {export_folder}{' '.join(file_cmds)}"]
if export_folder
else [" ".join(file_cmds)]
)
if not test_mode or verbose:
run_cli(file_cmds, test_mode=True)
else:
with suppress_stdout():
Session().console.print(f"To ensure: {output}")
if output:
timestamp = datetime.now().timestamp()
stamp_str = str(timestamp).replace(".", "")
whole_path = Path(REPOSITORY_DIRECTORY / "integration_test_output")
whole_path.mkdir(parents=True, exist_ok=True)
first_cmd = file_cmds[0].split("/")[1]
with open(
whole_path / f"{stamp_str}_{first_cmd}_output.txt", "w"
) as output_file, contextlib.redirect_stdout(output_file):
run_cli(file_cmds, test_mode=True)
else:
run_cli(file_cmds, test_mode=True)
def replace_dynamic(match: re.Match, special_arguments: Dict[str, str]) -> str:
"""Replace ${key=default} with value in special_arguments if it exists, else with default.
Parameters
----------
match: re.Match[str]
The match object
special_arguments: Dict[str, str]
The key value pairs to replace in the scripts
Returns
----------
str
The new string
"""
cleaned = match[0].replace("{", "").replace("}", "").replace("$", "")
key, default = cleaned.split("=")
dict_value = special_arguments.get(key, default)
if dict_value:
return dict_value
return default
def run_routine(file: str, routines_args=Optional[str]):
"""Execute command routine from .openbb file."""
user_routine_path = Path(Session().user.preferences.export_directory, "routines")
default_routine_path = ASSETS_DIRECTORY / "routines" / file
if user_routine_path.exists():
run_scripts(path=user_routine_path, routines_args=routines_args)
elif default_routine_path.exists():
run_scripts(path=default_routine_path, routines_args=routines_args)
else:
Session().console.print(
f"Routine not found, please put your `.openbb` file into : {user_routine_path}."
)
# pylint: disable=unused-argument
def main(
debug: bool,
dev: bool,
path_list: List[str],
routines_args: Optional[List[str]] = None,
**kwargs,
):
"""Run the CLI with various options.
Parameters
----------
debug : bool
Whether to run the CLI in debug mode
dev:
Points backend towards development environment instead of production
test : bool
Whether to run the CLI in integrated test mode
filtert : str
Filter test files with given string in name
paths : List[str]
The paths to run for scripts or to test
verbose : bool
Whether to show output from tests
routines_args : List[str]
One or multiple inputs to be replaced in the routine and separated by commas.
E.g. GME,AMC,BTC-USD
"""
if debug:
Session().settings.DEBUG_MODE = True
if dev:
Session().settings.DEV_BACKEND = True
Session().settings.BASE_URL = "https://payments.openbb.dev/"
Session().settings.HUB_URL = "https://my.openbb.dev"
if isinstance(path_list, list) and path_list[0].endswith(".openbb"):
run_routine(file=path_list[0], routines_args=routines_args)
elif path_list:
argv_cmds = list([" ".join(path_list).replace(" /", "/home/")])
argv_cmds = insert_start_slash(argv_cmds) if argv_cmds else argv_cmds
run_cli(argv_cmds)
else:
run_cli()
def parse_args_and_run():
"""Parse input arguments and run CLI."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="cli",
description="The OpenBB Platform CLI.",
)
parser.add_argument(
"-d",
"--debug",
dest="debug",
action="store_true",
default=False,
help="Runs the CLI in debug mode.",
)
parser.add_argument(
"--dev",
dest="dev",
action="store_true",
default=False,
help="Points backend towards development environment instead of production",
)
parser.add_argument(
"--file",
help="The path or .openbb file to run.",
dest="path",
nargs="+",
default="",
type=str,
)
parser.add_argument(
"-i",
"--input",
help=(
"Select multiple inputs to be replaced in the routine and separated by commas."
"E.g. GME,AMC,BTC-USD"
),
dest="routine_args",
type=lambda s: [str(item) for item in s.split(",")],
default=None,
)
parser.add_argument(
"-t",
"--test",
action="store_true",
help=(
"Run the CLI in testing mode. Also run this option and '-h'"
" to see testing argument options."
),
)
# The args -m, -f and --HistoryManager.hist_file are used only in reports menu
# by papermill and that's why they have suppress help.
parser.add_argument(
"-m",
help=argparse.SUPPRESS,
dest="module",
default="",
type=str,
)
parser.add_argument(
"-f",
help=argparse.SUPPRESS,
dest="module_file",
default="",
type=str,
)
parser.add_argument(
"--HistoryManager.hist_file",
help=argparse.SUPPRESS,
dest="module_hist_file",
default="",
type=str,
)
if sys.argv[1:] and "-" not in sys.argv[1][0]:
sys.argv.insert(1, "--file")
ns_parser, unknown = parser.parse_known_args()
# This ensures that if cli.py receives unknown args it will not start.
# Use -d flag if you want to see the unknown args.
if unknown:
if ns_parser.debug:
Session().console.print(unknown)
else:
sys.exit(-1)
main(
ns_parser.debug,
ns_parser.dev,
ns_parser.path,
ns_parser.routine_args,
module=ns_parser.module,
module_file=ns_parser.module_file,
module_hist_file=ns_parser.module_hist_file,
)
def launch(
debug: bool = False, dev: bool = False, queue: Optional[List[str]] = None
) -> None:
"""Launch CLI."""
if queue:
main(debug, dev, queue, module="")
else:
parse_args_and_run()
if __name__ == "__main__":
parse_args_and_run()

View File

@@ -0,0 +1,293 @@
"""Feature Flags Controller Module."""
import argparse
from typing import List, Optional
from openbb_cli.config.completer import NestedCompleter
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.session import Session
class FeatureFlagsController(BaseController):
"""Feature Flags Controller class."""
CHOICES_COMMANDS: List[str] = [
"retryload",
"tab",
"interactive",
"cls",
"watermark",
"promptkit",
"thoughts",
"reporthtml",
"exithelp",
"rcontext",
"richpanel",
"tbhint",
"overwrite",
"version",
"console_style",
"flair",
"timezone",
"language",
"n_rows",
"n_cols",
]
PATH = "/settings/"
CHOICES_GENERATION = True
def __init__(self, queue: Optional[List[str]] = None):
"""Initialize the Constructor."""
super().__init__(queue)
if Session().prompt_session and Session().settings.USE_PROMPT_TOOLKIT:
choices: dict = self.choices_default
self.completer = NestedCompleter.from_nested_dict(choices)
def print_help(self):
"""Print help."""
settings = Session().settings
mt = MenuText("settings/")
mt.add_info("_info_")
mt.add_raw("\n")
mt.add_setting("interactive", settings.USE_INTERACTIVE_DF)
mt.add_setting("cls", settings.USE_CLEAR_AFTER_CMD)
mt.add_setting("promptkit", settings.USE_PROMPT_TOOLKIT)
mt.add_setting("exithelp", settings.ENABLE_EXIT_AUTO_HELP)
mt.add_setting("rcontext", settings.REMEMBER_CONTEXTS)
mt.add_setting("richpanel", settings.ENABLE_RICH_PANEL)
mt.add_setting("tbhint", settings.TOOLBAR_HINT)
mt.add_setting("overwrite", settings.FILE_OVERWRITE)
mt.add_setting("version", settings.SHOW_VERSION)
mt.add_raw("\n")
mt.add_info("_settings_")
mt.add_raw("\n")
mt.add_cmd("console_style")
mt.add_cmd("flair")
mt.add_cmd("timezone")
mt.add_cmd("language")
mt.add_cmd("n_rows")
mt.add_cmd("n_cols")
Session().console.print(text=mt.menu_text, menu="Feature Flags")
def call_overwrite(self, _):
"""Process overwrite command."""
Session().settings.set_item(
"FILE_OVERWRITE", not Session().settings.FILE_OVERWRITE
)
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_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 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,
)
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."
)
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_language(self, other_args: List[str]) -> None:
"""Process language command."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="language",
description="Change your custom language.",
add_help=False,
)
parser.add_argument(
"-l",
"--language",
dest="language",
action="store",
required=False,
type=str,
)
ns_parser = self.parse_simple_args(parser, other_args)
if ns_parser and ns_parser.language:
Session().settings.set_item("USE_LANGUAGE", ns_parser.language)
elif not other_args:
Session().console.print(
f"Current language: {Session().settings.USE_LANGUAGE}"
)
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 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}"
)

View File

@@ -0,0 +1,104 @@
"""Routines handler module."""
from typing import Optional
import requests
from openbb_cli.config.constants import (
CONNECTION_ERROR_MSG,
CONNECTION_TIMEOUT_MSG,
TIMEOUT,
)
from openbb_cli.session import Session
# created dictionaries for personal and default routines with the structure
# {"file_name" :["script","personal/default"]}
# and stored dictionaries in list
# created new directory structure to account for personal and default routines
# pylint: disable=too-many-arguments
def upload_routine(
auth_header: str,
name: str = "",
description: str = "",
routine: str = "",
override: bool = False,
tags: str = "",
public: bool = False,
timeout: int = TIMEOUT,
) -> Optional[requests.Response]:
"""Send a routine to the server.
Parameters
----------
auth_header : str
The authorization header, e.g. "Bearer <token>".
name : str
The name of the routine.
routine : str
The routine.
override : bool
Whether to override the routine if it already exists.
tags : str
The tags of the routine.
public : bool
Whether to make the routine public or not.
timeout : int
The timeout, by default TIMEOUT
Returns
-------
Optional[requests.Response]
The response from the post request.
"""
data = {
"name": name,
"description": description,
"script": routine,
"override": override,
"tags": tags,
"version": Session().settings.VERSION,
"public": public,
}
_console = Session().console
try:
response = requests.post(
headers={"Authorization": auth_header},
url=Session().settings.BASE_URL + "/terminal/script",
json=data,
timeout=timeout,
)
if response.status_code == 200:
username = getattr(Session().user.profile.hub_session, "username", None)
if not username:
_console.print("[red]No username found.[/red]")
_console.print("[red]Failed to upload your routine.[/red]")
return None
_console.print("[green]Successfully uploaded your routine.[/]")
hub_url = Session().settings.HUB_URL
if public:
_console.print(
f"\n[yellow]Share or edit it at {hub_url}/u/{username}/routine/{name.replace(' ', '-')}[/]"
)
else:
_console.print(f"\n[yellow]Go to {hub_url} to edit this script,[/]")
_console.print(
f"[yellow]or even make it public so you can access it at "
f"{hub_url}/u/{username}/routine/{name.replace(' ', '-')}[/]"
)
elif response.status_code != 409: # 409: routine already exists
_console.print(
"[red]" + response.json().get("detail", "Unknown error.") + "[/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 upload your routine.[/red]")
return None

View File

@@ -0,0 +1,58 @@
"""Platform controller factory to create a platform controller."""
from typing import Dict, List, Union
from openbb_cli.argparse_translator.argparse_class_processor import (
ArgparseClassProcessor,
)
from openbb_cli.controllers.base_platform_controller import PlatformController
class PlatformControllerFactory:
"""Factory to create a platform controller."""
def __init__(self, platform_router: type, **kwargs):
"""Create the controller name."""
self.platform_router = platform_router
self._translated_target = ArgparseClassProcessor(
target_class=self.platform_router, reference=kwargs.get("reference", {})
)
self.router_name = (
str(type(self.platform_router))
.rsplit(".", maxsplit=1)[-1]
.replace("'>", "")
.replace("ROUTER_", "")
.lower()
)
self.controller_name = f"{self.router_name.capitalize()}Controller"
def create(self) -> type:
"""Create the platform controller."""
ClassName = self.controller_name
Parents = (PlatformController,)
Attributes: Dict[str, Union[bool, List[str]]] = {"CHOICES_GENERATION": True}
# Menu and Command choices generation
choices_menus: List[str] = []
choices_commands: List[str] = []
translators = self._translated_target.translators
paths = self._translated_target.paths
# menus
for key, value in paths.items():
if value == "path":
continue
choices_menus.append(key)
# commands
for name, _ in translators.items():
if any(f"{self.router_name}_{path}" in name for path in paths):
continue
new_name = name.replace(f"{self.router_name}_", "")
choices_commands.append(new_name)
Attributes["CHOICES_MENUS"] = choices_menus
Attributes["CHOICES_COMMANDS"] = choices_commands
# Use type to create the class
DynamicClass = type(ClassName, Parents, Attributes)
return DynamicClass

View File

@@ -0,0 +1,486 @@
"""Routine functions for OpenBB Platform CLI."""
import re
from datetime import datetime, timedelta
from typing import Dict, List, Match, Optional, Tuple, Union
from dateutil.relativedelta import relativedelta
from openbb_cli.session import Session
# pylint: disable=too-many-statements,eval-used,consider-iterating-dictionary
# pylint: disable=too-many-branches,too-many-return-statements
# Necessary for OpenBB keywords
MONTHS_VALUE = {
"JANUARY": 1,
"FEBRUARY": 2,
"MARCH": 3,
"APRIL": 4,
"MAY": 5,
"JUNE": 6,
"JULY": 7,
"AUGUST": 8,
"SEPTEMBER": 9,
"OCTOBER": 10,
"NOVEMBER": 11,
"DECEMBER": 12,
}
WEEKDAY_VALUE = {
"MONDAY": 0,
"TUESDAY": 1,
"WEDNESDAY": 2,
"THURSDAY": 3,
"FRIDAY": 4,
"SATURDAY": 5,
"SUNDAY": 6,
}
def is_reset(command: str) -> bool:
"""Test whether a command is a reset command.
Parameters
----------
command : str
The command to test
Returns
-------
answer : bool
Whether the command is a reset command
"""
if "reset" in command:
return True
if command == "r":
return True
if command == "r\n":
return True
return False
def match_and_return_openbb_keyword_date(keyword: str) -> str: # noqa: PLR0911
"""Return OpenBB keyword into date.
Parameters
----------
keyword : str
String with potential OpenBB keyword (e.g. 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY)
Returns
----------
str: Date with format YYYY-MM-DD
"""
now = datetime.now()
for i, regex in enumerate([r"^\$(\d+)([A-Z]+)AGO$", r"^\$(\d+)([A-Z]+)FROMNOW$"]):
match = re.match(regex, keyword)
if match:
integer_value = int(match.group(1))
time_unit = match.group(2)
clean_time = time_unit.upper()
if "DAYS" in clean_time or "MONTHS" in clean_time or "YEARS" in clean_time:
kwargs = {time_unit.lower(): integer_value}
if i == 0:
return (now - relativedelta(**kwargs)).strftime("%Y-%m-%d") # type: ignore
return (now + relativedelta(**kwargs)).strftime("%Y-%m-%d") # type: ignore
match = re.search(r"\$LAST(\w+)", keyword)
if match:
time_unit = match.group(1)
# Check if it corresponds to a month
if time_unit in list(MONTHS_VALUE.keys()):
the_year = now.year
# Calculate the year and month for last month date
if now.month <= MONTHS_VALUE[time_unit]:
# If the current month is greater than the last date month, it means it is this year
the_year = now.year - 1
return datetime(the_year, MONTHS_VALUE[time_unit], 1).strftime("%Y-%m-%d")
# Check if it corresponds to a week day
if time_unit in list(WEEKDAY_VALUE.keys()):
if datetime.weekday(now) > WEEKDAY_VALUE[time_unit]:
return (
now
- timedelta(datetime.weekday(now))
+ timedelta(WEEKDAY_VALUE[time_unit])
).strftime("%Y-%m-%d")
return (
now
- timedelta(7)
- timedelta(datetime.weekday(now))
+ timedelta(WEEKDAY_VALUE[time_unit])
).strftime("%Y-%m-%d")
match = re.search(r"\$NEXT(\w+)", keyword)
if match:
time_unit = match.group(1)
# Check if it corresponds to a month
if time_unit in list(MONTHS_VALUE.keys()):
# Calculate the year and month for next month date
if now.month < MONTHS_VALUE[time_unit]:
# If the current month is greater than the last date month, it means it is this year
return datetime(now.year, MONTHS_VALUE[time_unit], 1).strftime(
"%Y-%m-%d"
)
return datetime(now.year + 1, MONTHS_VALUE[time_unit], 1).strftime(
"%Y-%m-%d"
)
# Check if it corresponds to a week day
if time_unit in list(WEEKDAY_VALUE.keys()):
if datetime.weekday(now) < WEEKDAY_VALUE[time_unit]:
return (
now
- timedelta(datetime.weekday(now))
+ timedelta(WEEKDAY_VALUE[time_unit])
).strftime("%Y-%m-%d")
return (
now
+ timedelta(7)
- timedelta(datetime.weekday(now))
+ timedelta(WEEKDAY_VALUE[time_unit])
).strftime("%Y-%m-%d")
return ""
def parse_openbb_script( # noqa: PLR0911,PLR0912
raw_lines: List[str],
script_inputs: Optional[List[str]] = None,
) -> Tuple[str, str]:
"""Parse .openbb script.
Parameters
----------
raw_lines : List[str]
Lines from .openbb script
script_inputs: str, optional
Inputs to the script that come externally
Returns
-------
str
Error that occurred - if empty means no error
str
Processed string from .openbb script that can be run by the OpenBB Platform CLI
"""
ROUTINE_VARS: Dict[str, Union[str, List[str]]] = dict()
if script_inputs:
ROUTINE_VARS["$ARGV"] = script_inputs
## PRE PROCESSING
# Remove reset commands, comments, empty lines and trailing/leading whitespaces
raw_lines = [
x.strip()
for x in raw_lines
if (not is_reset(x)) and ("#" not in x) and x.strip()
]
## LOOK FOR NEW VARIABLES BEING DECLARED FROM USERS
lines_without_declarations = list()
for line in raw_lines:
# Check if this line has a variable attribution
# This currently allows user to override ARGV parameter
if "$" in line and "=" in line:
match = re.search(r"\$(\w+)\s*=\s*([\w\d,-.\s]+)", line)
if match:
VAR_NAME = match.group(1)
VAR_VALUES = match.group(2)
ROUTINE_VARS["$" + VAR_NAME] = (
VAR_VALUES if "," not in VAR_VALUES else VAR_VALUES.split(",")
)
# Just throw a warning when user uses wrong convention
numdollars = len(re.findall(r"\$", line))
if numdollars > 1:
Session().console.print(
f"The variable {VAR_NAME} should not be declared as "
f"{'$' * numdollars}{VAR_NAME}. Instead it will be "
f"converted into ${VAR_NAME}."
)
else:
lines_without_declarations.append(line)
else:
lines_without_declarations.append(line)
# At this stage our ROUTINE_VARS should be completed coming from external AND from internal
# Now we want to replace the ROUTINE_VARS to where applicable throughout the .openbb script
# Due to this implementation, a variable declared at the end will still be effective
lines_with_vars_replaced = list()
foreach_loop_found = False
for line in lines_without_declarations:
# Save temporary line to ensure that all vars get replaced by correct vars
templine = line
# Found 'end' keyword which means that a loop has terminated
if re.match(r"^\s*end\s*$", line, re.IGNORECASE):
# Check whether the foreach loop has started or not
if not foreach_loop_found:
return (
"[red]The script has a foreach loop that terminates before it gets started. "
"Add the keyword 'foreach' to explicitly start loop[/red]",
"",
)
foreach_loop_found = False
else:
# Found 'foreach' keyword which means there needs to be a matching 'end'
if re.search(r"foreach", line, re.IGNORECASE):
foreach_loop_found = True
# Regular expression pattern to match variables starting with $
pattern = r"(?<!\$)(\$(\w+)(\[[^\]]*\])?)(?=(?:[^\]]*\]*))"
# Find all matches of the pattern in the line
matches: Optional[List[Match[str]]] = re.findall(pattern, line)
if matches:
for match in matches:
if match:
VAR_NAME = "$" + match[1]
VAR_SLICE = match[2][1:-1] if match[2] else ""
# Within a list refers to a single element
if VAR_SLICE.isdigit():
# This is an edge case for when the user has a variable such as $DATE = 2022-01-01
# We want the user to be able to access it with $DATE or $DATE[0] and the latest
# in python will only take the first '2'
if VAR_SLICE == "0":
if VAR_NAME in ROUTINE_VARS:
values = eval( # noqa: S307
f'ROUTINE_VARS["{VAR_NAME}"]'
)
if isinstance(values, list):
templine = templine.replace(
match[0],
eval(f"values[{VAR_SLICE}]"), # noqa: S307
)
else:
templine = templine.replace(match[0], values)
else:
return (
f"[red]Variable {VAR_NAME} not given "
"for current routine script.[/red]",
"",
)
# Only enters here when any other index from 0 is used
elif VAR_NAME in ROUTINE_VARS:
variable = eval( # noqa: S307
f'ROUTINE_VARS["{VAR_NAME}"]'
)
length_variable = (
len(variable) if isinstance(variable, list) else 1
)
# We use <= because we are using 0 index based lists
if length_variable <= int(VAR_SLICE):
return (
f"[red]Variable {VAR_NAME} only has "
f"{length_variable} elements and there "
f"was an attempt to access it with index {VAR_SLICE}.[/red]",
"",
)
templine = templine.replace(
match[0],
variable[int(VAR_SLICE)],
)
else:
return (
f"[red]Variable {VAR_NAME} not given for current routine script.[/red]",
"",
)
# Involves slicing which is a bit more tricky to use eval on
elif (
":" in VAR_SLICE
and len(VAR_SLICE.split(":")) == 2
and (
VAR_SLICE.split(":")[0].isdigit()
or VAR_SLICE.split(":")[1].isdigit()
)
):
slicing_tuple = "slice("
slicing_tuple += (
VAR_SLICE.split(":")[0]
if VAR_SLICE.split(":")[0].isdigit()
else "None"
)
slicing_tuple += ","
slicing_tuple += (
VAR_SLICE.split(":")[1]
if VAR_SLICE.split(":")[1].isdigit()
else "None"
)
slicing_tuple += ")"
vars_to_loop = eval( # noqa: S307
f'ROUTINE_VARS["{VAR_NAME}"][{slicing_tuple}]'
)
# Check whether the slicing was successful or not
if vars_to_loop:
templine = templine.replace(
match[0],
",".join(vars_to_loop),
)
else:
return (
f"[red]The foreach loop cannot run with input: {match[0]}.[/red]",
"",
)
# Just replace value without slicing or list
else:
if VAR_SLICE:
# Check if the string starts with a minus sign
if VAR_SLICE.startswith("-"):
if not VAR_SLICE[1:].isdigit():
return (
f"[red]Index '{VAR_SLICE}' is not a value[/red]",
"",
)
if int(VAR_SLICE) < 0:
return (
f"[red]Negative index on {VAR_NAME} is not allowed[/red]",
"",
)
if not VAR_SLICE.isdigit():
return (
f"[red]Index '{VAR_SLICE}' is not a value[/red]",
"",
)
if VAR_NAME in ROUTINE_VARS:
value = eval( # noqa: S307
f'ROUTINE_VARS["{VAR_NAME}"]'
)
# If the value is a list, we want to replace it with the whole list
if isinstance(value, list):
templine = templine.replace(
match[0],
",".join(value),
)
else:
templine = templine.replace(match[0], value)
else:
# Check if this is an OpenBB keyword variable like
# 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY
# and decode it into the right date if it exists
potential_date_match = (
match_and_return_openbb_keyword_date(VAR_NAME)
)
if potential_date_match:
templine = templine.replace(
match[0], potential_date_match
)
else:
return (
f"[red]Variable {VAR_NAME} not given for "
"current routine script.[/red]",
"",
)
lines_with_vars_replaced.append(templine)
# If this flags ends in True it means that the script routine has a foreach loop that never terminates
if foreach_loop_found:
return (
"[red]The script has a foreach loop that doesn't terminate. "
"Add the keyword 'end' to explicitly terminate loop[/red]",
"",
)
# Finally the only remaining thing to address are the foreach loops. For that we'll go through
# those lines and unroll the arguments that will be iterated by.
# Note that the fact that we checked before that the amount of foreach and end matches allow us
# to be confident that the script has no clear issues.
within_foreach = False
foreach_lines_loop: List[str] = list()
parsed_script = ""
final_lines = list()
varname = "VAR"
varused_inside = False
for line in lines_with_vars_replaced:
# Found 'foreach' header associated with loop
match = re.search(
r"foreach \$\$([A-Za-z\_]+) in ([A-Za-z0-9,-.]+)", line, re.IGNORECASE
)
if match:
varname = match.group(1)
foreach_loop = match.group(2).split(",")
within_foreach = True
# We are inside a loop and this is a line that we will want to replicate,
# so we need to temporarily store it until we reach the end
elif within_foreach:
# Found 'end' keyword which means that the foreach loop has reached the end
if re.match(r"^\s*end\s*$", line, re.IGNORECASE):
# Now we want to process what we were waiting for before
# Iterate through main foreach header
for var in foreach_loop:
# Iterate through all lines within foreach and end loop
for foreach_line_loop in foreach_lines_loop:
if f"$${varname}" in foreach_line_loop:
final_lines.append(
foreach_line_loop.replace(f"$${varname}", var).strip()
)
varused_inside = True
elif "$$" in foreach_line_loop:
return (
"[red]The script has a foreach loop that iterates through "
f"{','.join(foreach_loop)} with variable $${varname} "
"but another var name is being utilized instead[/red]",
"",
)
else:
final_lines.append(foreach_line_loop.strip())
if not varused_inside:
Session().console.print(
f"The variable {varname} was used in foreach header "
"but it wasn't used inside the loop."
)
varused_inside = False
# Since this has been processed we reset the foreach loop lines
within_foreach = False
foreach_lines_loop = list()
else:
foreach_lines_loop.append(line)
else:
final_lines.append(line)
# If the list is non null, then we want to convert this into a parsed string that is
# recognized by the OpenBB Platform CLI
if final_lines:
parsed_script = f"{'/'.join([line.rstrip() for line in final_lines])}".replace(
"//", "/home/"
)
if parsed_script[0] == "/":
# If the user had added a / at the beginning, then it was converted to //home/
# and we need to remove it
if parsed_script.startswith("//home"):
parsed_script = parsed_script[6:]
else:
# We want the script to start from the home menu, hence we add it if the user
# didn't add it
parsed_script = "/" + parsed_script
# If the script finishes with // it means that we converted it to /home/
# This means that we are expecting a command to follow, but since this is
# the end of the script, we need to remove the trailing /
if parsed_script.endswith("/home/"):
parsed_script = parsed_script[:-1]
return "", parsed_script

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
"""Settings model"""
from typing import Any
from dotenv import dotenv_values, set_key
from openbb_cli.config.constants import (
ENV_FILE_PROJECT,
ENV_FILE_REPOSITORY,
ENV_FILE_SETTINGS,
)
from pydantic import BaseModel, ConfigDict, model_validator
class Settings(BaseModel):
"""Settings model."""
# Platform CLI version
VERSION: str = "1.0.0"
# DEVELOPMENT FLAGS
TEST_MODE: bool = False
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
# GENERAL
TIMEZONE: str = "America/New_York"
FLAIR: str = ":openbb"
USE_LANGUAGE: str = "en"
PREVIOUS_USE: bool = False
# STYLE
RICH_STYLE: str = "dark"
# OUTPUT
ALLOWED_NUMBER_OF_ROWS: int = 366
ALLOWED_NUMBER_OF_COLUMNS: int = 15
# OPENBB
HUB_URL: str = "https://my.openbb.co"
BASE_URL: str = "https://payments.openbb.co"
model_config = ConfigDict(validate_assignment=True)
def __repr__(self) -> str:
"""Return a string representation of the model."""
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
)
@model_validator(mode="before")
@classmethod
def from_env(cls, values: dict) -> dict:
"""Load .env files.
Loads the dotenv files in the following order:
1. Repository .env file
2. Package .env file
3. User .env file
"""
settings = {}
settings.update(dotenv_values(ENV_FILE_REPOSITORY))
settings.update(dotenv_values(ENV_FILE_PROJECT))
settings.update(dotenv_values(ENV_FILE_SETTINGS))
settings.update(values)
filtered = {k.replace("OPENBB_", ""): v for k, v in settings.items()}
return filtered
def set_item(self, key: str, value: Any) -> None:
"""Set an item in the model and save to .env."""
# TODO: Check if this is ok, we are just saving into the settings .env
# Same behavior as before...
setattr(self, key, value)
set_key(str(ENV_FILE_SETTINGS), "OPENBB_" + key, str(value))

79
cli/openbb_cli/session.py Normal file
View File

@@ -0,0 +1,79 @@
"""Settings module."""
import sys
from pathlib import Path
from typing import Optional
from openbb import obb
from openbb_core.app.model.abstract.singleton import SingletonMeta
from openbb_core.app.model.user_settings import UserSettings as User
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
from openbb_cli.config.console import Console
from openbb_cli.config.constants import HIST_FILE_PROMPT
from openbb_cli.config.style import Style
from openbb_cli.models.settings import Settings
class Session(metaclass=SingletonMeta):
"""Session class."""
def __init__(self):
"""Initialize session."""
self._obb = obb
self._settings = Settings()
self._style = Style(
style=self._settings.RICH_STYLE,
directory=Path(self._obb.user.preferences.user_styles_directory), # type: ignore[union-attr]
)
self._console = Console(
settings=self._settings, style=self._style.console_style
)
self._prompt_session = self._get_prompt_session()
@property
def user(self) -> User:
"""Get platform user."""
return self._obb.user # type: ignore[union-attr]
@property
def settings(self) -> Settings:
"""Get CLI settings."""
return self._settings
@property
def style(self) -> Style:
"""Get CLI style."""
return self._style
@property
def console(self) -> Console:
"""Get console."""
return self._console
@property
def prompt_session(self) -> Optional[PromptSession]:
"""Get prompt session."""
return self._prompt_session
def _get_prompt_session(self) -> Optional[PromptSession]:
"""Initialize prompt session."""
try:
if sys.stdin.isatty():
prompt_session: Optional[PromptSession] = PromptSession(
history=FileHistory(str(HIST_FILE_PROMPT))
)
else:
prompt_session = None
except Exception:
prompt_session = None
return prompt_session
def is_local(self) -> bool:
"""Check if user is local."""
return not bool(self.user.profile.hub_session)
def reset(self) -> None:
pass