Skip to content

Commit

Permalink
[Feature] - Custom deprecation (OpenBB-finance#6005)
Browse files Browse the repository at this point in the history
* custom deprecation

* custom deprecation

* using the new deprecation

* custom deprecation on the package builder

* remove comment

* ruff

* black

* static assets

* tests

* using parametrization instead

* test for deprecated endpoints (OpenBB-finance#6014)

* Deprecation warning on the reference docs (OpenBB-finance#6015)

* typo/fix

* bring back methods needed for markdown generation

* add deprecation warning to docs

* contributor docs for deprecating endpoints - tks @deeleeramone

* small changes on publishing procedure per @the-praxs

* moving the deprecation summary class to deprecation file instead

* explanation on class variables

* Update website/content/platform/development/contributor-guidelines/deprecating_endpoints.md

Co-authored-by: Pratyush Shukla <[email protected]>

* Update website/content/platform/development/contributor-guidelines/deprecating_endpoints.md

Co-authored-by: Pratyush Shukla <[email protected]>

* Update website/content/platform/development/contributor-guidelines/deprecating_endpoints.md

Co-authored-by: Pratyush Shukla <[email protected]>

* Update openbb_platform/openbb/package/index.py

Co-authored-by: Pratyush Shukla <[email protected]>

* Update website/content/platform/development/contributor-guidelines/deprecating_endpoints.md

Co-authored-by: Pratyush Shukla <[email protected]>

* Update website/content/platform/development/contributor-guidelines/deprecating_endpoints.md

Co-authored-by: Pratyush Shukla <[email protected]>

* deprecating on 4.3 instead @the-praxs

---------

Co-authored-by: Igor Radovanovic <[email protected]>
Co-authored-by: Pratyush Shukla <[email protected]>
  • Loading branch information
3 people authored and luqmanbello committed Feb 1, 2024
1 parent f4890b2 commit 3c36b79
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 27 deletions.
8 changes: 5 additions & 3 deletions build/pypi/openbb_platform/PUBLISH.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Publishing checklist:
2. Ensure all integration tests pass: `pytest openbb_platform -m integration`
3. Run `python -c "import openbb; openbb.build()"` to build the static assets. Make sure that only required extensions are installed.

> **Note** Run `python -c "import openbb"` after building the static to check that no additional static is being built.
> **Note** Run `python -c "import openbb"` after building the static to check that no additional static is being built.
4. Run the following commands for publishing the packages to PyPI:

Expand All @@ -17,11 +17,13 @@ Publishing checklist:

1. For the core package run: `python build/pypi/openbb_platform/publish.py --core`
2. For the extension and provider packages run: `python build/pypi/openbb_platform/publish.py --extensions`
3. For the `openbb` package, do the following
3. For the `openbb` package - **which requires manual publishing** - do the following
- Bump the dependency package versions
- Re-build the static assets that are bundled with the package
- Run unit tests to validate the existence of deprecated endpoints

> Note that, in order for packages to pick up the latest versions of dependencies, it might be necessary to clear the local cache of the dependencies:
> [!TIP]
> Note that, in order for packages to pick up the latest versions of dependencies, it is advised to clear the local cache of the dependencies:
>
> We can do that with `pip cache purge` and `poetry cache clear pypi --all`
>
Expand Down
62 changes: 62 additions & 0 deletions openbb_platform/core/openbb_core/app/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
OpenBB-specific deprecation warnings.
This implementation was inspired from Pydantic's specific warnings and modified to suit OpenBB's needs.
"""

from typing import Optional, Tuple


class DeprecationSummary(str):
"""A string subclass that can be used to store deprecation metadata."""

def __new__(cls, value, metadata):
"""Create a new instance of the class."""
obj = str.__new__(cls, value)
obj.metadata = metadata
return obj


class OpenBBDeprecationWarning(DeprecationWarning):
"""
A OpenBB specific deprecation warning.
This warning is raised when using deprecated functionality in OpenBB. It provides information on when the
deprecation was introduced and the expected version in which the corresponding functionality will be removed.
Attributes
----------
message: Description of the warning.
since: Version in what the deprecation was introduced.
expected_removal: Version in what the corresponding functionality expected to be removed.
"""

# The choice to use class variables is based on the potential for extending the class in future developments.
# Example: launching Platform V5 and decide to create a subclimagine we areass named OpenBBDeprecatedSinceV4,
# which inherits from OpenBBDeprecationWarning. In this subclass, we would set since=4.X and expected_removal=5.0.
# It's important for these values to be defined at the class level, rather than just at the instance level,
# to ensure consistency and clarity in our deprecation warnings across the platform.

message: str
since: Tuple[int, int]
expected_removal: Tuple[int, int]

def __init__(
self,
message: str,
*args: object,
since: Tuple[int, int],
expected_removal: Optional[Tuple[int, int]] = None,
) -> None:
super().__init__(message, *args)
self.message = message.rstrip(".")
self.since = since
self.expected_removal = expected_removal or (since[0] + 1, 0)
self.long_message = (
f"{self.message}. Deprecated in OpenBB Platform V{self.since[0]}.{self.since[1]}"
f" to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}."
)

def __str__(self) -> str:
"""Return the warning message."""
return self.long_message
15 changes: 7 additions & 8 deletions openbb_platform/core/openbb_core/app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pydantic.v1.validators import find_validators
from typing_extensions import Annotated, ParamSpec, _AnnotatedAlias

from openbb_core.app.deprecation import DeprecationSummary, OpenBBDeprecationWarning
from openbb_core.app.example_generator import ExampleGenerator
from openbb_core.app.extension_loader import ExtensionLoader
from openbb_core.app.model.abstract.warning import OpenBBWarning
Expand Down Expand Up @@ -232,7 +233,6 @@ def command(
api_router = self._api_router

model = kwargs.pop("model", "")
deprecation_message = kwargs.pop("deprecation_message", None)
examples = kwargs.pop("examples", [])
exclude_auto_examples = kwargs.pop("exclude_auto_examples", False)

Expand Down Expand Up @@ -279,14 +279,13 @@ def command(
},
)

# For custom deprecation messages
# For custom deprecation
if kwargs.get("deprecated", False):
if deprecation_message:
kwargs["summary"] = deprecation_message
else:
kwargs["summary"] = (
"This functionality will be deprecated in the future releases."
)
deprecation: OpenBBDeprecationWarning = kwargs.pop("deprecation")

kwargs["summary"] = DeprecationSummary(
deprecation.long_message, deprecation
)

api_router.add_api_route(**kwargs)

Expand Down
146 changes: 142 additions & 4 deletions openbb_platform/core/openbb_core/app/static/package_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
from json import dumps, load
from pathlib import Path
from typing import (
Any,
Callable,
Dict,
List,
Literal,
Optional,
OrderedDict,
Set,
Expand All @@ -20,6 +22,7 @@
TypeVar,
Union,
get_args,
get_origin,
get_type_hints,
)

Expand All @@ -35,7 +38,7 @@
from openbb_core.app.extension_loader import ExtensionLoader, OpenBBGroups
from openbb_core.app.model.custom_parameter import OpenBBCustomParameter
from openbb_core.app.provider_interface import ProviderInterface
from openbb_core.app.router import RouterLoader
from openbb_core.app.router import CommandMap, RouterLoader
from openbb_core.app.static.utils.console import Console
from openbb_core.app.static.utils.linters import Linters
from openbb_core.env import Env
Expand Down Expand Up @@ -302,6 +305,8 @@ def get_path_hint_type_list(cls, path: str) -> List[Type]:
for child_path in child_path_list:
route = PathHandler.get_route(path=child_path, route_map=route_map)
if route:
if route.deprecated:
hint_type_list.append(type(route.summary.metadata))
function_hint_type_list = cls.get_function_hint_type_list(func=route.endpoint) # type: ignore
hint_type_list.extend(function_hint_type_list)

Expand Down Expand Up @@ -331,10 +336,11 @@ def build(cls, path: str) -> str:
code += "\nimport typing"
code += "\nfrom typing import List, Dict, Union, Optional, Literal"
code += "\nfrom annotated_types import Ge, Le, Gt, Lt"
code += "\nfrom warnings import warn, simplefilter"
if sys.version_info < (3, 9):
code += "\nimport typing_extensions"
else:
code += "\nfrom typing_extensions import Annotated"
code += "\nfrom typing_extensions import Annotated, deprecated"
code += "\nfrom openbb_core.app.utils import df_to_basemodel"
code += "\nfrom openbb_core.app.static.utils.decorators import validate\n"
code += "\nfrom openbb_core.app.static.utils.filters import filter_inputs\n"
Expand All @@ -347,6 +353,15 @@ def build(cls, path: str) -> str:
module_list = list(set(module_list))
module_list.sort()

specific_imports = {
hint_type.__module__: hint_type.__name__
for hint_type in hint_type_list
if getattr(hint_type, "__name__", None) is not None
}
code += "\n"
for module, name in specific_imports.items():
code += f"from {module} import {name}\n"

code += "\n"
for module in module_list:
code += f"import {module}\n"
Expand Down Expand Up @@ -644,6 +659,7 @@ def build_command_method_signature(
func_name: str,
formatted_params: OrderedDict[str, Parameter],
return_type: type,
path: str,
model_name: Optional[str] = None,
) -> str:
"""Build the command method signature."""
Expand All @@ -658,7 +674,20 @@ def build_command_method_signature(
if "pandas.DataFrame" in func_params
else ""
)
code = f"\n @validate{args}"

msg = ""
if MethodDefinition.is_deprecated_function(path):
deprecation_message = MethodDefinition.get_deprecation_message(path)
deprecation_type_class = type(
deprecation_message.metadata # type: ignore
).__name__

msg = "\n @deprecated("
msg += f'\n "{deprecation_message}",'
msg += f"\n category={deprecation_type_class},"
msg += "\n )"

code = f"\n @validate{args}{msg}"
code += f"\n def {func_name}("
code += f"\n self,\n {func_params}\n ) -> {func_returns}:\n"

Expand Down Expand Up @@ -705,7 +734,7 @@ def build_command_method_body(path: str, func: Callable):

if MethodDefinition.is_deprecated_function(path):
deprecation_message = MethodDefinition.get_deprecation_message(path)
code += " from warnings import warn, simplefilter; simplefilter('always', DeprecationWarning)\n"
code += " simplefilter('always', DeprecationWarning)\n"
code += f""" warn("{deprecation_message}", category=DeprecationWarning, stacklevel=2)\n\n"""

code += " return self._run(\n"
Expand Down Expand Up @@ -755,6 +784,7 @@ def build_command_method(
func_name=func_name,
formatted_params=formatted_params,
return_type=sig.return_annotation,
path=path,
model_name=model_name,
)
code += cls.build_command_method_doc(
Expand Down Expand Up @@ -964,6 +994,114 @@ def generate(
return doc
return doc

@staticmethod
def get_model_standard_params(param_fields: Dict[str, FieldInfo]) -> Dict[str, Any]:
"""Get the test params for the fetcher based on the required standard params."""
test_params: Dict[str, Any] = {}
for field_name, field in param_fields.items():
if field.default and field.default is not PydanticUndefined:
test_params[field_name] = field.default
elif field.default and field.default is PydanticUndefined:
example_dict = {
"symbol": "AAPL",
"symbols": "AAPL,MSFT",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"country": "Portugal",
"date": "2023-01-01",
"countries": ["portugal", "spain"],
}
if field_name in example_dict:
test_params[field_name] = example_dict[field_name]
elif field.annotation == str:
test_params[field_name] = "TEST_STRING"
elif field.annotation == int:
test_params[field_name] = 1
elif field.annotation == float:
test_params[field_name] = 1.0
elif field.annotation == bool:
test_params[field_name] = True
elif get_origin(field.annotation) is Literal: # type: ignore
option = field.annotation.__args__[0] # type: ignore
if isinstance(option, str):
test_params[field_name] = f'"{option}"'
else:
test_params[field_name] = option

return test_params

@staticmethod
def get_full_command_name(route: str) -> str:
"""Get the full command name."""
cmd_parts = route.split("/")
del cmd_parts[0]

menu = cmd_parts[0]
command = cmd_parts[-1]
sub_menus = cmd_parts[1:-1]

sub_menu_str_cmd = f".{'.'.join(sub_menus)}" if sub_menus else ""

full_command = f"{menu}{sub_menu_str_cmd}.{command}"

return full_command

@classmethod
def generate_example(
cls,
model_name: str,
standard_params: Dict[str, FieldInfo],
) -> str:
"""Generate the example for the command."""
# find the model router here
cm = CommandMap()
commands_model = cm.commands_model
route = [k for k, v in commands_model.items() if v == model_name]

if not route:
return ""

full_command_name = cls.get_full_command_name(route=route[0])
example_params = cls.get_model_standard_params(param_fields=standard_params)

# Edge cases (might find more)
if "crypto" in route[0] and "symbol" in example_params:
example_params["symbol"] = "BTCUSD"
elif "currency" in route[0] and "symbol" in example_params:
example_params["symbol"] = "EURUSD"
elif (
"index" in route[0]
and "european" not in route[0]
and "symbol" in example_params
):
example_params["symbol"] = "SPX"
elif (
"index" in route[0]
and "european" in route[0]
and "symbol" in example_params
):
example_params["symbol"] = "BUKBUS"
elif (
"futures" in route[0] and "curve" in route[0] and "symbol" in example_params
):
example_params["symbol"] = "VX"
elif "futures" in route[0] and "symbol" in example_params:
example_params["symbol"] = "ES"

example = "\n Example\n -------\n"
example += " >>> from openbb import obb\n"
example += f" >>> obb.{full_command_name}("
for param_name, param_value in example_params.items():
if isinstance(param_value, str):
param_value = f'"{param_value}"' # noqa: PLW2901
example += f"{param_name}={param_value}, "
if example_params:
example = example[:-2] + ")\n"
else:
example += ")\n"

return example


class PathHandler:
"""Handle the paths for the Platform."""
Expand Down
25 changes: 24 additions & 1 deletion openbb_platform/core/tests/app/static/test_package_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,10 @@ def test_build_func_returns(method_definition, return_type, expected_output):
assert output == expected_output


def test_build_command_method_signature(method_definition):
@patch("openbb_core.app.static.package_builder.MethodDefinition")
def test_build_command_method_signature(mock_method_definitions, method_definition):
"""Test build command method signature."""
mock_method_definitions.is_deprecated_function.return_value = False
formatted_params = {
"param1": Parameter("NoneType", kind=Parameter.POSITIONAL_OR_KEYWORD),
"param2": Parameter("int", kind=Parameter.POSITIONAL_OR_KEYWORD),
Expand All @@ -272,10 +274,31 @@ def test_build_command_method_signature(method_definition):
func_name="test_func",
formatted_params=formatted_params,
return_type=return_type,
path="test_path",
)
assert output


@patch("openbb_core.app.static.package_builder.MethodDefinition")
def test_build_command_method_signature_deprecated(
mock_method_definitions, method_definition
):
"""Test build command method signature."""
mock_method_definitions.is_deprecated_function.return_value = True
formatted_params = {
"param1": Parameter("NoneType", kind=Parameter.POSITIONAL_OR_KEYWORD),
"param2": Parameter("int", kind=Parameter.POSITIONAL_OR_KEYWORD),
}
return_type = int
output = method_definition.build_command_method_signature(
func_name="test_func",
formatted_params=formatted_params,
return_type=return_type,
path="test_path",
)
assert "@deprecated" in output


def test_build_command_method_doc(method_definition):
"""Test build command method doc."""

Expand Down
Loading

0 comments on commit 3c36b79

Please sign in to comment.