Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] - Support for custom examples in router commands #5993

Merged
merged 19 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions openbb_platform/core/openbb_core/app/assets/parameter_pool.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"crypto": {
"symbol": "BTCUSD"
},
"currency": {
"symbol": "EURUSD"
},
"derivatives": {
"symbol": "AAPL"
},
"economy": {
"country": "portugal",
"countries": ["portugal", "spain"]
},
"economy.fred_series": {
"symbol": "GFDGDPA188S"
},
"equity": {
"symbol": "AAPL",
"symbols": "AAPL,MSFT",
"query": "AAPL"
},
"equity.fundamental.historical_attributes": {
"tag": "ebitda"
},
"equity.fundamental.latest_attributes": {
"tag": "ceo"
},
"equity.fundamental.transcript": {
"year": 2020
},
"etf": {
"symbol": "SPY",
"query": "Vanguard"
},
"futures": {
"symbol": "ES"
},
"index": {
"symbol": "SPX",
"index": "^IBEX"
},
"news": {
"symbols": "AAPL,MSFT"
},
"regulators": {
"symbol": "AAPL",
"query": "AAPL"
}
}
1 change: 1 addition & 0 deletions openbb_platform/core/openbb_core/app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pathlib import Path

ASSETS_DIRECTORY = Path(__file__).parent / "assets"
HOME_DIRECTORY = Path.home()
OPENBB_DIRECTORY = Path(HOME_DIRECTORY, ".openbb_platform")
USER_SETTINGS_PATH = Path(OPENBB_DIRECTORY, "user_settings.json")
Expand Down
83 changes: 83 additions & 0 deletions openbb_platform/core/openbb_core/app/example_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""OpenBB Platform example generator."""

import json
from pathlib import Path
from typing import (
Any,
Dict,
)

from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined

from openbb_core.app.constants import ASSETS_DIRECTORY
from openbb_core.app.provider_interface import ProviderInterface

try:
with Path(ASSETS_DIRECTORY, "parameter_pool.json").open() as f:
PARAMETER_POOL = json.load(f)
except Exception:
PARAMETER_POOL = {}


class ExampleGenerator:
"""Generate examples for the API."""

@staticmethod
def _get_value_from_pool(pool: dict, route: str, param: str) -> str:
"""Get the value from the pool.

The example parameters can be defined for:
- route: "crypto.historical.price": {"symbol": "CRYPTO_HISTORICAL_PRICE_SYMBOL"}
- sub-router: "crypto.historical": {"symbol": "CRYPTO_HISTORICAL_SYMBOL"}
- router: "crypto": {"symbol": "CRYPTO_SYMBOL"}

The search for the 'key' is done in the following order:
- route
- sub-router
- router
"""
parts = route.split(".")
for i in range(len(parts), 0, -1):
partial_route = ".".join(parts[:i])
if partial_route in pool and param in pool[partial_route]:
return pool[partial_route][param]
return "VALUE_NOT_FOUND"

@classmethod
def generate(
cls,
route: str,
model: str,
) -> str:
"""Generate the example for the command."""
if not route or not model:
return ""

standard_params: Dict[str, FieldInfo] = (
ProviderInterface()
.map.get(model, {})
.get("openbb", {})
.get("QueryParams", {})
.get("fields", {})
)

eg_params: Dict[str, Any] = {}
for p, v in standard_params.items():
if v.default is not None:
if v.default is not PydanticUndefined and v.default != "":
eg_params[p] = v.default
else:
eg_params[p] = cls._get_value_from_pool(PARAMETER_POOL, route, p)

example = f"obb.{route}("
for n, v in eg_params.items():
if isinstance(v, str):
v = f'"{v}"' # noqa: PLW2901
example += f"{n}={v}, "
if eg_params:
example = example[:-2] + ")"
else:
example += ")"

return example
32 changes: 20 additions & 12 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.example_generator import ExampleGenerator
from openbb_core.app.extension_loader import ExtensionLoader
from openbb_core.app.model.abstract.warning import OpenBBWarning
from openbb_core.app.model.command_context import CommandContext
Expand Down Expand Up @@ -232,16 +233,23 @@ def command(

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

if func := SignatureInspector.complete(func, model):
if not exclude_auto_examples:
examples.insert(
0,
ExampleGenerator.generate(
route=SignatureInspector.get_operation_id(func, sep="."),
model=model,
),
)

if model:
kwargs["response_model_exclude_unset"] = True
kwargs["openapi_extra"] = {"model": model}

func = SignatureInspector.complete_signature(func, model)

if func:
CommandValidator.check(func=func, model=model)

kwargs["openapi_extra"] = kwargs.get("openapi_extra", {})
kwargs["openapi_extra"]["model"] = model
kwargs["openapi_extra"]["examples"] = examples
kwargs["operation_id"] = kwargs.get(
"operation_id", SignatureInspector.get_operation_id(func)
)
Expand Down Expand Up @@ -300,7 +308,7 @@ class SignatureInspector:
"""Inspect function signature."""

@classmethod
def complete_signature(
def complete(
cls, func: Callable[P, OBBject], model: str
) -> Optional[Callable[P, OBBject]]:
"""Complete function signature."""
Expand All @@ -321,7 +329,6 @@ def complete_signature(
category=OpenBBWarning,
)
return None

cls.validate_signature(
func,
{
Expand Down Expand Up @@ -445,19 +452,20 @@ def get_description(func: Callable) -> str:
if doc:
description = doc.split(" Parameters\n ----------")[0]
description = description.split(" Returns\n -------")[0]
description = description.split(" Example\n -------")[0]
description = "\n".join([line.strip() for line in description.split("\n")])

return description
return ""

@staticmethod
def get_operation_id(func: Callable) -> str:
def get_operation_id(func: Callable, sep: str = "_") -> str:
"""Get operation id."""
operation_id = [
t.replace("_router", "").replace("openbb_", "")
for t in func.__module__.split(".") + [func.__name__]
]
cleaned_id = "_".join({c: "" for c in operation_id if c}.keys())
cleaned_id = sep.join({c: "" for c in operation_id if c}.keys())
return cleaned_id


Expand Down
Loading
Loading