diff options
37 files changed, 607 insertions, 810 deletions
diff --git a/openbb_platform/core/openbb_core/app/model/fast_api_settings.py b/openbb_platform/core/openbb_core/app/model/api_settings.py index 24ad76a3916..1eccb73eaef 100644 --- a/openbb_platform/core/openbb_core/app/model/fast_api_settings.py +++ b/openbb_platform/core/openbb_core/app/model/api_settings.py @@ -24,7 +24,7 @@ class Servers(BaseModel): description: str = "Local OpenBB development server" -class FastAPISettings(BaseModel): +class APISettings(BaseModel): """Settings model for FastAPI configuration.""" model_config = ConfigDict(frozen=True) diff --git a/openbb_platform/core/openbb_core/app/model/custom_parameter.py b/openbb_platform/core/openbb_core/app/model/custom_parameter.py deleted file mode 100644 index 58cfb3faa53..00000000000 --- a/openbb_platform/core/openbb_core/app/model/custom_parameter.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Custom parameter and choices for OpenBB.""" - -import sys -from dataclasses import dataclass -from typing import Dict, Optional - -from typing_extensions import LiteralString - -# `slots` is available on Python >= 3.10 -if sys.version_info >= (3, 10): - slots_true = {"slots": True} -else: - slots_true: Dict[str, bool] = {} - - -class BaseMetadata: - """Base class for all metadata. - - This exists mainly so that implementers - can do `isinstance(..., BaseMetadata)` while traversing field annotations. - """ - - __slots__ = () - - -@dataclass(frozen=True, **slots_true) -class OpenBBCustomParameter(BaseMetadata): - """Custom parameter for OpenBB.""" - - description: Optional[str] = None - - -@dataclass(frozen=True, **slots_true) -class OpenBBCustomChoices(BaseMetadata): - """Custom choices for OpenBB.""" - - choices: Optional[LiteralString] = None diff --git a/openbb_platform/core/openbb_core/app/model/field.py b/openbb_platform/core/openbb_core/app/model/field.py new file mode 100644 index 00000000000..d791cb08f08 --- /dev/null +++ b/openbb_platform/core/openbb_core/app/model/field.py @@ -0,0 +1,28 @@ +"""Custom field for OpenBB.""" + +from typing import Any, List, Optional + +from pydantic.fields import FieldInfo + + +class OpenBBField(FieldInfo): + """Custom field for OpenBB.""" + + def __repr__(self): + """Override FieldInfo __repr__.""" + # We use repr() to avoid decoding special characters like \n + if self.choices: + return f"OpenBBField(description={repr(self.description)}, choices={repr(self.choices)})" + return f"OpenBBField(description={repr(self.description)})" + + def __init__(self, description: str, choices: Optional[List[Any]] = None): + """Initialize OpenBBField.""" + json_schema_extra = {"choices": choices} if choices else None + super().__init__(description=description, json_schema_extra=json_schema_extra) # type: ignore[arg-type] + + @property + def choices(self) -> Optional[List[Any]]: + """Custom choices.""" + if self.json_schema_extra: + return self.json_schema_extra.get("choices") # type: ignore[union-attr,return-value] + return None diff --git a/openbb_platform/core/openbb_core/app/model/obbject.py b/openbb_platform/core/openbb_core/app/model/obbject.py index 92038ea4970..2db09e0cd00 100644 --- a/openbb_platform/core/openbb_core/app/model/obbject.py +++ b/openbb_platform/core/openbb_core/app/model/obbject.py @@ -243,6 +243,24 @@ class OBBject(Tagged, Generic[T]): del results["index"] return results + def to_llm(self) -> Union[Dict[Hashable, Any], List[Dict[Hashable, Any]]]: + """Convert results field to an LLM compatible output. + + Returns + ------- + Union[Dict[Hashable, Any], List[Dict[Hashable, Any]]] + Dictionary of lists or list of dictionaries if orient is "records". + """ + df = self.to_dataframe(index=None) + + results = df.to_json( + orient="records", + date_format="iso", + date_unit="s", + ) + + return results + def show(self, **kwargs: Any) -> None: """Display chart.""" # pylint: disable=no-member diff --git a/openbb_platform/core/openbb_core/app/model/preferences.py b/openbb_platform/core/openbb_core/app/model/preferences.py index 57987020b17..19c2d7f745d 100644 --- a/openbb_platform/core/openbb_core/app/model/preferences.py +++ b/openbb_platform/core/openbb_core/app/model/preferences.py @@ -9,24 +9,28 @@ from pydantic import BaseModel, ConfigDict, Field, PositiveInt class Preferences(BaseModel): """Preferences for the OpenBB platform.""" - data_directory: str = str(Path.home() / "OpenBBUserData") - export_directory: str = str(Path.home() / "OpenBBUserData" / "exports") - user_styles_directory: str = str(Path.home() / "OpenBBUserData" / "styles" / "user") cache_directory: str = str(Path.home() / "OpenBBUserData" / "cache") chart_style: Literal["dark", "light"] = "dark" + data_directory: str = str(Path.home() / "OpenBBUserData") + export_directory: str = str(Path.home() / "OpenBBUserData" / "exports") + metadata: bool = True + output_type: Literal[ + "OBBject", "dataframe", "polars", "numpy", "dict", "chart", "llm" + ] = Field( + default="OBBject", + description="Python default output type.", + validate_default=True, + ) plot_enable_pywry: bool = True - plot_pywry_width: PositiveInt = 1400 - plot_pywry_height: PositiveInt = 762 plot_open_export: bool = ( False # Whether to open plot image exports after they are created ) - table_style: Literal["dark", "light"] = "dark" + plot_pywry_height: PositiveInt = 762 + plot_pywry_width: PositiveInt = 1400 request_timeout: PositiveInt = 15 - metadata: bool = True - output_type: Literal["OBBject", "dataframe", "polars", "numpy", "dict", "chart"] = ( - Field(default="OBBject", description="Python default output type.") - ) show_warnings: bool = True + table_style: Literal["dark", "light"] = "dark" + user_styles_directory: str = str(Path.home() / "OpenBBUserData" / "styles" / "user") model_config = ConfigDict(validate_assignment=True) diff --git a/openbb_platform/core/openbb_core/app/model/python_settings.py b/openbb_platform/core/openbb_core/app/model/python_settings.py new file mode 100644 index 00000000000..f03648c92b4 --- /dev/null +++ b/openbb_platform/core/openbb_core/app/model/python_settings.py @@ -0,0 +1,23 @@ +"""Python configuration settings model.""" + +from typing import List, Optional + +from pydantic import BaseModel, Field, PositiveInt + + +class PythonSettings(BaseModel): + """Settings model for Python interface configuration.""" + + docstring_sections: List[str] = Field( + default_factory=lambda: ["description", "parameters", "returns", "examples"], + description="Sections to include in autogenerated docstrings.", + ) + docstring_max_length: Optional[PositiveInt] = Field( + default=None, description="Maximum length of autogenerated docstrings." + ) + + 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() + ) diff --git a/openbb_platform/core/openbb_core/app/model/system_settings.py b/openbb_platform/core/openbb_core/app/model/system_settings.py index db30f6c20df..c5439fb760d 100644 --- a/openbb_platform/core/openbb_core/app/model/system_settings.py +++ b/openbb_platform/core/openbb_core/app/model/system_settings.py @@ -14,7 +14,8 @@ from openbb_core.app.constants import ( USER_SETTINGS_PATH, ) from openbb_core.app.model.abstract.tagged import Tagged -from openbb_core.app.model.fast_api_settings import FastAPISettings +from openbb_core.app.model.api_settings import APISettings +from openbb_core.app.model.python_settings import PythonSettings from openbb_core.app.version import CORE_VERSION, VERSION @@ -46,7 +47,10 @@ class SystemSettings(Tagged): log_collect: bool = True # API section - api_settings: FastAPISettings = Field(default_factory=FastAPISettings) + api_settings: APISettings = Field(default_factory=APISettings) + + # Python section + python_settings: PythonSettings = Field(default_factory=PythonSettings) # Others debug_mode: bool = False diff --git a/openbb_platform/core/openbb_core/app/service/system_service.py b/openbb_platform/core/openbb_core/app/service/system_service.py index fd7b565b588..906ff6808ec 100644 --- a/openbb_platform/core/openbb_core/app/service/system_service.py +++ b/openbb_platform/core/openbb_core/app/service/system_service.py @@ -20,6 +20,7 @@ class SystemService(metaclass=SingletonMeta): "headless", "logging_sub_app", "api_settings", + "python_settings", "debug_mode", } diff --git a/openbb_platform/core/openbb_core/app/static/package_builder.py b/openbb_platform/core/openbb_core/app/static/package_builder.py index 52073a05cff..8a2a86a6619 100644 --- a/openbb_platform/core/openbb_core/app/static/package_builder.py +++ b/openbb_platform/core/openbb_core/app/static/package_builder.py @@ -6,7 +6,7 @@ import inspect import re import shutil import sys -from dataclasses import Field +from dataclasses import Field as DCField from functools import partial from inspect import Parameter, _empty, isclass, signature from json import dumps, load @@ -38,14 +38,12 @@ from starlette.routing import BaseRoute from typing_extensions import Annotated, _AnnotatedAlias from openbb_core.app.extension_loader import ExtensionLoader, OpenBBGroups -from openbb_core.app.model.custom_parameter import ( - OpenBBCustomChoices, - OpenBBCustomParameter, -) from openbb_core.app.model.example import Example +from openbb_core.app.model.field import OpenBBField from openbb_core.app.model.obbject import OBBject from openbb_core.app.provider_interface import ProviderInterface from openbb_core.app.router import RouterLoader +from openbb_core.app.service.system_service import SystemService from openbb_core.app.static.utils.console import Console from openbb_core.app.static.utils.linters import Linters from openbb_core.app.version import CORE_VERSION, VERSION @@ -354,7 +352,6 @@ class ImportDefinition: hint_type_list = cls.get_path_hint_type_list(path=path) code = "from openbb_core.app.static.container import Container" code += "\nfrom openbb_core.app.model.obbject import OBBject" - code += "\nfrom openbb_core.app.model.custom_parameter import OpenBBCustomParameter, OpenBBCustomChoices" # These imports were not detected before build, so we add them manually and # ruff --fix the resulting code to remove unused imports. @@ -363,6 +360,7 @@ class ImportDefinition: code += "\nimport pandas" code += "\nimport numpy" code += "\nimport datetime" + code += "\nfrom datetime import date" code += "\nimport pydantic" code += "\nfrom pydantic import BaseModel" code += "\nfrom inspect import Parameter" @@ -379,6 +377,7 @@ class ImportDefinition: code += "\nfrom openbb_core.app.static.utils.filters import filter_inputs\n" code += "\nfrom openbb_core.provider.abstract.data import Data" code += "\nfrom openbb_core.app.deprecation import OpenBBDeprecationWarning\n" + code += "\nfrom openbb_core.app.model.field import OpenBBField" if path.startswith("/quantitative"): code += "\nfrom openbb_quantitative.models import " code += "(CAPMModel,NormalityModel,OmegaModel,SummaryModel,UnitRootModel)" @@ -581,8 +580,8 @@ class MethodDefinition: kind=Parameter.POSITIONAL_OR_KEYWORD, annotation=Annotated[ bool, - OpenBBCustomParameter( - description="Whether to create a chart or not, by default False." + OpenBBField( + description="Whether to create a chart or not, by default False.", ), ], default=False, @@ -604,14 +603,14 @@ class MethodDefinition: name="provider", kind=Parameter.POSITIONAL_OR_KEYWORD, annotation=Annotated[ - Union[MethodDefinition.get_type(field), None], - OpenBBCustomParameter( + Optional[MethodDefinition.get_type(field)], + OpenBBField( description=( "The provider to use for the query, by default None.\n" f" If None, the provider specified in defaults is selected or '{first}' if there is\n" " no default." "" - ) + ), ), ], default=None, @@ -662,7 +661,7 @@ class MethodDefinition: ): """Add the field custom description and choices to the param signature as annotations.""" if model_name: - available_fields: Dict[str, Field] = ( + available_fields: Dict[str, DCField] = ( ProviderInterface().params[model_name]["standard"].__dataclass_fields__ ) @@ -671,27 +670,26 @@ class MethodDefinition: continue field_default = available_fields[param].default - choices = getattr(field_default, "json_schema_extra", {}).get( "choices", [] ) description = getattr(field_default, "description", "") - if choices: - new_value = value.replace( - annotation=Annotated[ - value.annotation, - OpenBBCustomParameter(description=description), - OpenBBCustomChoices(choices=choices), - ], - ) - else: - new_value = value.replace( - annotation=Annotated[ - value.annotation, - OpenBBCustomParameter(description=description), - ], - ) + PartialParameter = partial( + OpenBBField, + description=description, + ) + + new_value = value.replace( + annotation=Annotated[ + value.annotation, + ( + PartialParameter(choices=choices) + if choices + else PartialParameter() + ), + ], + ) od[param] = new_value @@ -1044,6 +1042,7 @@ class DocstringGenerator: kwarg_params: dict, returns: Dict[str, FieldInfo], results_type: str, + sections: List[str], ) -> str: """Create the docstring for model.""" @@ -1077,50 +1076,56 @@ class DocstringGenerator: description = getattr(metadata[0], "description", "") if metadata else "" return type_, description - docstring = summary.strip("\n").replace("\n ", f"\n{create_indent(2)}") - docstring += "\n\n" - docstring += f"{create_indent(2)}Parameters\n" - docstring += f"{create_indent(2)}----------\n" - - # Explicit parameters - for param_name, param in explicit_params.items(): - type_, description = get_param_info(param) - type_str = format_type(str(type_), char_limit=79) - docstring += f"{create_indent(2)}{param_name} : {type_str}\n" - docstring += f"{create_indent(3)}{format_description(description)}\n" - - # Kwargs - for param_name, param in kwarg_params.items(): - p_type = getattr(param, "type", "") - type_ = ( - getattr(p_type, "__name__", "") if inspect.isclass(p_type) else p_type - ) + # Description summary + if "description" in sections: + docstring = summary.strip("\n").replace("\n ", f"\n{create_indent(2)}") + docstring += "\n\n" + if "parameters" in sections: + docstring += f"{create_indent(2)}Parameters\n" + docstring += f"{create_indent(2)}----------\n" + + # Explicit parameters + for param_name, param in explicit_params.items(): + type_, description = get_param_info(param) + type_str = format_type(str(type_), char_limit=79) + docstring += f"{create_indent(2)}{param_name} : {type_str}\n" + docstring += f"{create_indent(3)}{format_description(description)}\n" + + # Kwargs + for param_name, param in kwarg_params.items(): + p_type = getattr(param, "type", "") + type_ = ( + getattr(p_type, "__name__", "") + if inspect.isclass(p_type) + else p_type + ) - if "NoneType" in str(type_): - type_ = f"Optional[{type_}]".replace(", NoneType", "") - - default = getattr(param, "default", "") - description = getattr(default, "description", "") - docstring += f"{create_indent(2)}{param_name} : {type_}\n" - docstring += f"{create_indent(3)}{format_description(description)}\n" - - # Returns - docstring += "\n" - docstring += f"{create_indent(2)}Returns\n" - docstring += f"{create_indent(2)}-------\n" - providers, _ = get_param_info(explicit_params.get("provider", None)) - docstring += cls.get_OBBject_description(results_type, providers) - - # Schema - underline = "-" * len(model_name) - docstring += f"\n{create_indent(2)}{model_name}\n" - docstring += f"{create_indent(2)}{underline}\n"< |