From 5c0b36cddbbafb9a201b401df5d2272b236d9020 Mon Sep 17 00:00:00 2001 From: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> Date: Tue, 7 May 2024 14:22:44 +0200 Subject: [PATCH 1/2] [BugFix] - Fix lowercase symbols (#6342) * Fix lowercase symbols * fix decorator for to_upper * add annotated results with underlying symbol info * ? * Improvements --------- Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com> Co-authored-by: Henrique Joaquim --- .../standard_models/options_chains.py | 2 +- .../cboe/openbb_cboe/models/options_chains.py | 69 +++++++++++++++---- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py b/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py index 231945419ed..6a23f687f5d 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py @@ -21,8 +21,8 @@ class OptionsChainsQueryParams(QueryParams): symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", "")) - @classmethod @field_validator("symbol", mode="before", check_fields=False) + @classmethod def to_upper(cls, v: str) -> str: """Return the symbol in uppercase.""" return v.upper() diff --git a/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py b/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py index 1f83a7cb99a..c1c0602d3cc 100644 --- a/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py +++ b/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py @@ -10,6 +10,7 @@ from openbb_cboe.utils.helpers import ( get_company_directory, get_index_directory, ) +from openbb_core.provider.abstract.annotated_result import AnnotatedResult from openbb_core.provider.abstract.fetcher import Fetcher from openbb_core.provider.standard_models.options_chains import ( OptionsChainsData, @@ -62,10 +63,9 @@ class CboeOptionsChainsFetcher( query: CboeOptionsChainsQueryParams, credentials: Optional[Dict[str, str]], **kwargs: Any, - ) -> List[Dict]: - """Return the raw data from the Cboe endpoint""" - - symbol = query.symbol.replace("^", "").split(",")[0] + ) -> Dict: + """Return the raw data from the Cboe endpoint.""" + symbol = query.symbol.replace("^", "").split(",")[0].upper() INDEXES = await get_index_directory(use_cache=query.use_cache) SYMBOLS = await get_company_directory(use_cache=query.use_cache) INDEXES = INDEXES.set_index("index_symbol") @@ -78,21 +78,57 @@ class CboeOptionsChainsFetcher( if symbol in TICKER_EXCEPTIONS or symbol in INDEXES.index else f"https://cdn.cboe.com/api/global/delayed_quotes/options/{symbol}.json" ) - - return await amake_request(quotes_url) + results = await amake_request(quotes_url) + return results # type: ignore @staticmethod def transform_data( query: CboeOptionsChainsQueryParams, - data: List[Dict], + data: Dict, **kwargs: Any, - ) -> List[CboeOptionsChainsData]: - """Transform the data to the standard format""" - + ) -> AnnotatedResult[List[CboeOptionsChainsData]]: + """Transform the data to the standard format.""" if not data: raise EmptyDataError() + results_metadata = {} + options = data.get("data", {}).pop("options", []) + change_percent = data["data"].get("percent_change", None) + iv30_percent = data["data"].get("iv30_change_percent", None) + if change_percent: + change_percent = change_percent / 100 + if iv30_percent: + iv30_percent = iv30_percent / 100 + last_timestamp = data["data"].get("last_trade_time", None) + if last_timestamp: + last_timestamp = to_datetime( + last_timestamp, format="%Y-%m-%dT%H:%M:%S" + ).strftime("%Y-%m-%d %H:%M:%S") + results_metadata.update( + { + "symbol": data["data"].get("symbol"), + "security_type": data["data"].get("security_type", None), + "bid": data["data"].get("bid", None), + "bid_size": data["data"].get("bid_size", None), + "ask": data["data"].get("ask", None), + "ask_size": data["data"].get("ask_size", None), + "open": data["data"].get("open", None), + "high": data["data"].get("high", None), + "low": data["data"].get("low", None), + "close": data["data"].get("close", None), + "volume": data["data"].get("volume", None), + "current_price": data["data"].get("current_price", None), + "prev_close": data["data"].get("prev_day_close", None), + "change": data["data"].get("price_change", None), + "change_percent": change_percent, + "iv30": data["data"].get("iv30", None), + "iv30_change": data["data"].get("iv30_change", None), + "iv30_change_percent": iv30_percent, + "last_tick": data["data"].get("tick", None), + "last_trade_timestamp": last_timestamp, + } + ) - options_df = DataFrame.from_records(data["data"]["options"]) + options_df = DataFrame.from_records(options) options_df = options_df.rename( columns={ @@ -152,7 +188,10 @@ class CboeOptionsChainsFetcher( quotes["prev_close"] = round(quotes["prev_close"], 2) quotes["change_percent"] = round(quotes["change_percent"] / 100, 4) - return [ - CboeOptionsChainsData.model_validate(d) - for d in quotes.reset_index().to_dict("records") - ] + return AnnotatedResult( + result=[ + CboeOptionsChainsData.model_validate(d) + for d in quotes.reset_index().to_dict("records") + ], + metadata=results_metadata, + ) From 756eebdf07c3a57664f526c4371302011bbb70d8 Mon Sep 17 00:00:00 2001 From: Henrique Joaquim Date: Tue, 7 May 2024 14:57:35 +0100 Subject: [PATCH 2/2] [Feature] Handle repeated non standard arguments (#6366) * remove about message * handle repeated arguments * accomodate all the choices from different providers --------- Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> --- .../argparse_translator.py | 103 +++++++++++++++--- cli/openbb_cli/controllers/base_controller.py | 5 - 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/cli/openbb_cli/argparse_translator/argparse_translator.py b/cli/openbb_cli/argparse_translator/argparse_translator.py index 45fa4ce5d6b..bf5917207d5 100644 --- a/cli/openbb_cli/argparse_translator/argparse_translator.py +++ b/cli/openbb_cli/argparse_translator/argparse_translator.py @@ -1,5 +1,6 @@ import argparse import inspect +import re from copy import deepcopy from enum import Enum from typing import ( @@ -21,6 +22,8 @@ from openbb_core.app.model.field import OpenBBField from pydantic import BaseModel, model_validator from typing_extensions import Annotated +# pylint: disable=protected-access + SEP = "__" @@ -200,7 +203,7 @@ class ArgparseTranslator: self.func = func self.signature = inspect.signature(func) self.type_hints = get_type_hints(func) - self.provider_parameters = [] + self.provider_parameters: List[str] = [] self._parser = argparse.ArgumentParser( prog=func.__name__, @@ -217,23 +220,95 @@ class ArgparseTranslator: 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) + self._handle_argument_in_groups(argument, argparse_group) - # 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 _handle_argument_in_groups(self, argument, group): + """Handle the argument and add it to the parser.""" - def _parser_arguments(self) -> List[str]: - """Get all the arguments from all groups currently defined on the parser.""" - arguments_in_use: List[str] = [] + def _in_optional_arguments(arg): + for action_group in self._parser._action_groups: + if action_group.title == "optional arguments": + for action in action_group._group_actions: + opts = action.option_strings + if (opts and opts[0] == arg) or action.dest == arg: + return True + return False - # 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) + def _remove_argument(arg) -> List[Optional[str]]: + groups_w_arg = [] - return arguments_in_use + # remove the argument from the parser + for action in self._parser._actions: + opts = action.option_strings + if (opts and opts[0] == arg) or action.dest == arg: + self._parser._remove_action(action) + break + + # remove from all groups + for action_group in self._parser._action_groups: + for action in action_group._group_actions: + opts = action.option_strings + if (opts and opts[0] == arg) or action.dest == arg: + action_group._group_actions.remove(action) + groups_w_arg.append(action_group.title) + + # remove from _action_groups dict + self._parser._option_string_actions.pop(f"--{arg}", None) + + return groups_w_arg + + def _get_arg_choices(arg) -> Tuple: + for action in self._parser._actions: + opts = action.option_strings + if (opts and opts[0] == arg) or action.dest == arg: + return tuple(action.choices or ()) + return () + + def _update_providers( + input_string: str, new_provider: List[Optional[str]] + ) -> str: + pattern = r"\(provider:\s*(.*?)\)" + providers = re.findall(pattern, input_string) + providers.extend(new_provider) + # remove pattern from help and add with new providers + input_string = re.sub(pattern, "", input_string).strip() + return f"{input_string} (provider: {', '.join(providers)})" + + # check if the argument is already in use, if not, add it + if f"--{argument.name}" not in self._parser._option_string_actions: + kwargs = argument.model_dump(exclude={"name"}, exclude_none=True) + group.add_argument(f"--{argument.name}", **kwargs) + self.provider_parameters.append(argument.name) + + else: + kwargs = argument.model_dump(exclude={"name"}, exclude_none=True) + model_choices = kwargs.get("choices", ()) or () + # extend choices + choices = tuple(set(_get_arg_choices(argument.name) + model_choices)) + + # check if the argument is in the optional arguments + if _in_optional_arguments(argument.name): + for action in self._parser._actions: + if action.dest == argument.name: + # update choices + action.choices = choices + # update help + action.help = _update_providers( + action.help or "", [group.title] + ) + return + + # if the argument is in use, remove it from all groups + # and return the groups that had the argument + groups_w_arg = _remove_argument(argument.name) + groups_w_arg.append(group.title) # add current group + + # add it to the optional arguments group instead + if choices: + kwargs["choices"] = choices # update choices + # add provider info to the help + kwargs["help"] = _update_providers(argument.help or "", groups_w_arg) + self._parser.add_argument(f"--{argument.name}", **kwargs) @property def parser(self) -> argparse.ArgumentParser: diff --git a/cli/openbb_cli/controllers/base_controller.py b/cli/openbb_cli/controllers/base_controller.py index 987f0cd413b..c5f083079ac 100644 --- a/cli/openbb_cli/controllers/base_controller.py +++ b/cli/openbb_cli/controllers/base_controller.py @@ -908,11 +908,6 @@ class BaseController(metaclass=ABCMeta): if "--help" in other_args or "-h" in other_args: txt_help = parser.format_help() + "\n" - if parser.prog != "about": - txt_help += ( - f"For more information and examples, use 'about {parser.prog}' " - f"to access the related guide.\n" - ) session.console.print(f"[help]{txt_help}[/help]") return None