mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
[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:
1
cli/openbb_cli/__init__.py
Normal file
1
cli/openbb_cli/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Package init"""
|
||||
0
cli/openbb_cli/argparse_translator/__init__.py
Normal file
0
cli/openbb_cli/argparse_translator/__init__.py
Normal file
148
cli/openbb_cli/argparse_translator/argparse_class_processor.py
Normal file
148
cli/openbb_cli/argparse_translator/argparse_class_processor.py
Normal 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"
|
||||
530
cli/openbb_cli/argparse_translator/argparse_translator.py
Normal file
530
cli/openbb_cli/argparse_translator/argparse_translator.py
Normal 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
|
||||
76
cli/openbb_cli/argparse_translator/obbject_registry.py
Normal file
76
cli/openbb_cli/argparse_translator/obbject_registry.py
Normal 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
36
cli/openbb_cli/assets/i18n/en.yml
vendored
Normal 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
|
||||
21
cli/openbb_cli/assets/routines/routine_example.openbb
vendored
Normal file
21
cli/openbb_cli/assets/routines/routine_example.openbb
vendored
Normal 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
|
||||
BIN
cli/openbb_cli/assets/styles/default/Consolas.ttf
vendored
Normal file
BIN
cli/openbb_cli/assets/styles/default/Consolas.ttf
vendored
Normal file
Binary file not shown.
47
cli/openbb_cli/assets/styles/default/dark.mpfstyle.json
vendored
Normal file
47
cli/openbb_cli/assets/styles/default/dark.mpfstyle.json
vendored
Normal 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
|
||||
}
|
||||
7
cli/openbb_cli/assets/styles/default/dark.mplrc.json
vendored
Normal file
7
cli/openbb_cli/assets/styles/default/dark.mplrc.json
vendored
Normal 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
|
||||
}
|
||||
96
cli/openbb_cli/assets/styles/default/dark.mplstyle
vendored
Normal file
96
cli/openbb_cli/assets/styles/default/dark.mplstyle
vendored
Normal 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
|
||||
132
cli/openbb_cli/assets/styles/default/dark.pltstyle.json
vendored
Normal file
132
cli/openbb_cli/assets/styles/default/dark.pltstyle.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
cli/openbb_cli/assets/styles/default/dark.richstyle.json
vendored
Normal file
9
cli/openbb_cli/assets/styles/default/dark.richstyle.json
vendored
Normal 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"
|
||||
}
|
||||
47
cli/openbb_cli/assets/styles/default/light.mpfstyle.json
vendored
Normal file
47
cli/openbb_cli/assets/styles/default/light.mpfstyle.json
vendored
Normal 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
|
||||
}
|
||||
7
cli/openbb_cli/assets/styles/default/light.mplrc.json
vendored
Normal file
7
cli/openbb_cli/assets/styles/default/light.mplrc.json
vendored
Normal 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
|
||||
}
|
||||
95
cli/openbb_cli/assets/styles/default/light.mplstyle
vendored
Normal file
95
cli/openbb_cli/assets/styles/default/light.mplstyle
vendored
Normal 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
|
||||
871
cli/openbb_cli/assets/styles/default/light.pltstyle.json
vendored
Normal file
871
cli/openbb_cli/assets/styles/default/light.pltstyle.json
vendored
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
cli/openbb_cli/assets/styles/default/light.richstyle.json
vendored
Normal file
9
cli/openbb_cli/assets/styles/default/light.richstyle.json
vendored
Normal 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"
|
||||
}
|
||||
102
cli/openbb_cli/assets/styles/default/tables.pltstyle.json
vendored
Normal file
102
cli/openbb_cli/assets/styles/default/tables.pltstyle.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
cli/openbb_cli/assets/styles/user/openbb.richstyle.json
vendored
Normal file
9
cli/openbb_cli/assets/styles/user/openbb.richstyle.json
vendored
Normal 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
20
cli/openbb_cli/cli.py
Normal 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()
|
||||
1
cli/openbb_cli/config/__init__.py
Normal file
1
cli/openbb_cli/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Core config init."""
|
||||
403
cli/openbb_cli/config/completer.py
Normal file
403
cli/openbb_cli/config/completer.py
Normal 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)
|
||||
97
cli/openbb_cli/config/console.py
Normal file
97
cli/openbb_cli/config/console.py
Normal 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()
|
||||
83
cli/openbb_cli/config/constants.py
Normal file
83
cli/openbb_cli/config/constants.py
Normal 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": "(☯)",
|
||||
}
|
||||
262
cli/openbb_cli/config/menu_text.py
Normal file
262
cli/openbb_cli/config/menu_text.py
Normal 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"
|
||||
85
cli/openbb_cli/config/setup.py
Normal file
85
cli/openbb_cli/config/setup.py
Normal 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)
|
||||
108
cli/openbb_cli/config/style.py
Normal file
108
cli/openbb_cli/config/style.py
Normal 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())
|
||||
1032
cli/openbb_cli/controllers/base_controller.py
Normal file
1032
cli/openbb_cli/controllers/base_controller.py
Normal file
File diff suppressed because it is too large
Load Diff
291
cli/openbb_cli/controllers/base_platform_controller.py
Normal file
291
cli/openbb_cli/controllers/base_platform_controller.py
Normal 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("")
|
||||
322
cli/openbb_cli/controllers/choices.py
Normal file
322
cli/openbb_cli/controllers/choices.py
Normal 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
|
||||
947
cli/openbb_cli/controllers/cli_controller.py
Normal file
947
cli/openbb_cli/controllers/cli_controller.py
Normal 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()
|
||||
293
cli/openbb_cli/controllers/feature_flags_controller.py
Normal file
293
cli/openbb_cli/controllers/feature_flags_controller.py
Normal 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}"
|
||||
)
|
||||
104
cli/openbb_cli/controllers/hub_service.py
Normal file
104
cli/openbb_cli/controllers/hub_service.py
Normal 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
|
||||
58
cli/openbb_cli/controllers/platform_controller_factory.py
Normal file
58
cli/openbb_cli/controllers/platform_controller_factory.py
Normal 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
|
||||
486
cli/openbb_cli/controllers/script_parser.py
Normal file
486
cli/openbb_cli/controllers/script_parser.py
Normal 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
|
||||
1056
cli/openbb_cli/controllers/utils.py
Normal file
1056
cli/openbb_cli/controllers/utils.py
Normal file
File diff suppressed because it is too large
Load Diff
85
cli/openbb_cli/models/settings.py
Normal file
85
cli/openbb_cli/models/settings.py
Normal 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
79
cli/openbb_cli/session.py
Normal 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
|
||||
Reference in New Issue
Block a user