add support for default providers by route

This commit is contained in:
montezdesousa
2023-07-08 18:05:56 +01:00
parent beccf6b9f0
commit d197acf220
11 changed files with 156 additions and 71 deletions

View File

@@ -69,7 +69,10 @@ async def setup_default_user_settings():
user_settings_repository.update(model=user_settings)
else:
default_user_settings = UserSettings(
profile=default_profile, credentials=default_user_settings.credentials
profile=default_profile,
credentials=default_user_settings.credentials,
preferences=default_user_settings.preferences,
defaults=default_user_settings.defaults,
)
user_settings_repository.create(model=default_user_settings)

View File

@@ -36,7 +36,8 @@ async def login_for_access_token(
if user_settings:
user_settings.profile.username = form_data.username
user_settings.profile.password_hash = get_password_hash(form_data.password)
user_service.user_settings_repository.create(user_settings)
updated_user_settings = UserService.update_default(user_settings)
user_service.user_settings_repository.create(updated_user_settings)
if not user_settings:
raise HTTPException(

View File

@@ -134,6 +134,30 @@ class ParametersBuilder:
return kwargs
@staticmethod
def update_provider_choices(
route: str,
kwargs: Dict[str, Any],
user_settings: UserSettings,
) -> Dict[str, Any]:
provider_choices = kwargs.get("provider_choices", None)
if provider_choices and isinstance(provider_choices, dict):
provider = provider_choices.get("provider", None)
if provider is None:
route_defaults = user_settings.defaults.routes.get(route, None)
if route_defaults is None:
raise ValueError(
f"'provider' not found: route '{route}' not found in settings.defaults."
)
provider = route_defaults.get("provider", None)
if provider is None:
raise ValueError(
f"'provider' not found: default 'provider' not found in settings.defaults for '{route}'."
)
kwargs["provider_choices"] = {"provider": provider}
return kwargs
@classmethod
def validate_kwargs(
cls,
@@ -166,6 +190,7 @@ class ParametersBuilder:
args: Tuple[Any],
execution_context: ExecutionContext,
func: Callable,
route: str,
kwargs: Dict[str, Any],
) -> Dict[str, Any]:
func = cls.get_polished_func(func=func)
@@ -190,6 +215,11 @@ class ParametersBuilder:
system_settings=system_settings,
user_settings=user_settings,
)
kwargs = cls.update_provider_choices(
route=route,
kwargs=kwargs,
user_settings=user_settings,
)
kwargs = cls.validate_kwargs(func=func, kwargs=kwargs)
return kwargs
@@ -221,6 +251,7 @@ class StaticCommandRunner:
args=args,
execution_context=execution_context,
func=func,
route=route,
kwargs=kwargs,
)
@@ -384,7 +415,11 @@ class CommandRunnerSession:
) -> None:
self._command_runner = command_runner or CommandRunner()
self._journal = journal or Journal()
self._user_settings = user_settings or UserService.read_default_user_settings()
self._user_settings = (
UserService.update_default(user_settings)
if user_settings
else UserService.read_default_user_settings()
)
@property
def command_runner(self) -> CommandRunner:

View File

@@ -1,16 +1,15 @@
from typing import Dict
from pydantic import BaseModel
from pydantic import BaseModel, Field
class Defaults(BaseModel):
"""Defaults."""
# TODO: Understand if this is good and if it should be in the hub or just locally
endpoints: Dict[str, Dict[str, str]] = {
"/stocks/load": {"provider": "polygon"},
"/stocks/news": {"provider": "fmp"},
}
class Config:
validate_assignment = True
routes: Dict[str, Dict[str, str]] = Field(default_factory=dict)
def __repr__(self) -> str:
return (

View File

@@ -92,10 +92,11 @@ class ProviderInterface:
return Literal[tuple(providers)] # type: ignore
@property
def provider_choices(self) -> type:
def provider_choices(self) -> ProviderChoices:
return make_dataclass(
cls_name="ProviderChoices",
fields=[("provider", self.providers_literal)],
bases=(ProviderChoices,),
)
@property

View File

@@ -1,5 +1,7 @@
import sys
from inspect import signature
from functools import partial
from inspect import Parameter, Signature, signature
from types import MappingProxyType
from typing import (
Annotated,
Any,
@@ -9,6 +11,7 @@ from typing import (
Optional,
Type,
get_args,
get_origin,
get_type_hints,
overload,
)
@@ -23,6 +26,7 @@ from pydantic.validators import find_validators
from openbb_sdk_core.app.model.command_context import CommandContext
from openbb_sdk_core.app.model.command_output import CommandOutput
from openbb_sdk_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
get_provider_interface,
@@ -66,21 +70,44 @@ class CommandValidator:
value_type=value_type
) or cls.is_valid_pydantic_model_type(model_type=value_type)
@staticmethod
def is_annotated_dc(annotation) -> bool:
return get_origin(annotation) == Annotated and hasattr(
annotation.__args__[0], "__dataclass_fields__"
)
@staticmethod
def check_reserved_param(
name: str,
expected_annot: Any,
parameter_map: MappingProxyType[str, Parameter],
func: Callable,
sig: Signature,
):
if name in parameter_map:
annotation = getattr(parameter_map[name], "annotation", None)
if annotation is not None and CommandValidator.is_annotated_dc(annotation):
annotation = annotation.__args__[0].__bases__[0]
if not annotation == expected_annot:
raise TypeError(
f"The parameter `{name}` must be a {expected_annot}.\n"
f"module = {func.__module__}\n"
f"function = {func.__name__}\n"
f"signature = {sig}\n"
)
@classmethod
def check_parameters(cls, func: Callable):
sig = signature(func)
parameter_map = sig.parameters
if (
"cc" in parameter_map
and not parameter_map["cc"].annotation == CommandContext
):
raise TypeError(
"The parameter `cc` must be a CommandContext.\n"
f"module = {func.__module__}\n"
f"function = {func.__name__}\n"
f"signature = {sig}\n"
)
check_res = partial(
cls.check_reserved_param, parameter_map=parameter_map, func=func, sig=sig
)
check_res("cc", CommandContext)
check_res("provider_choices", ProviderChoices)
check_res("standard_params", StandardParams)
check_res("extra_params", ExtraParams)
for parameter in parameter_map.values():
if not cls.is_serializable_value_type(value_type=parameter.annotation):

View File

@@ -1,5 +1,6 @@
from functools import reduce
from pathlib import Path
from typing import Optional
from typing import Any, Dict, List, MutableMapping, Optional
from openbb_sdk_core.app.model.user_settings import UserSettings
from openbb_sdk_core.app.repository.abstract.access_token_repository import (
@@ -26,7 +27,7 @@ from pymongo.mongo_client import MongoClient
class UserService:
REPOSITORY_DIRECTORY = Path(__file__).parent.parent.parent.parent
USER_SETTINGS_PATH = Path(REPOSITORY_DIRECTORY, "settings", "user_settings.json")
SYSTEM_SETTINGS_ALLOWED_FIELD_SET = {"credentials", "profile"}
SYSTEM_SETTINGS_ALLOWED_FIELD_SET = {"credentials", "preferences", "defaults"}
@staticmethod
def build_token_repository(
@@ -103,6 +104,33 @@ class UserService:
with path.open(mode="w") as file:
file.write(user_settings_json)
@classmethod
def update_default(cls, user_settings: UserSettings) -> UserSettings:
d1 = cls.read_default_user_settings().dict()
d2 = user_settings.dict() if user_settings else {}
updated = cls.merge_dicts([d1, d2])
return UserSettings.parse_obj(updated)
@staticmethod
def merge_dicts(list_of_dicts: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Merge a list of dictionaries."""
def recursive_merge(d1: Dict, d2: Dict) -> Dict:
"""Recursively merge dict d2 into dict d1 if d2 is value is not None."""
for k, v in d1.items():
if k in d2 and all(isinstance(e, MutableMapping) for e in (v, d2[k])):
d2[k] = recursive_merge(v, d2[k])
d3 = d1.copy()
d3.update((k, v) for k, v in d2.items() if v is not None)
return d3
result: Dict[str, Any] = {}
for d in list_of_dicts:
result = reduce(recursive_merge, (result, d))
return result
def __init__(
self,
mongodb_client: Optional[MongoClient] = None,

View File

@@ -173,7 +173,7 @@ class ImportDefinition:
code += "\nfrom inspect import Parameter"
code += "\nfrom typing import List, Dict, Union, Optional, Literal"
code += "\nfrom openbb_sdk_core.app.utils import df_to_basemodel"
code += "\nfrom openbb_sdk_core.app.static.utils import parse_command_input, parse_command_output\n"
code += "\nfrom openbb_sdk_core.app.static.parsers import parse_command_inputs, parse_command_output\n"
module_list = [hint_type.__module__ for hint_type in hint_type_list]
module_list = list(set(module_list))
@@ -377,7 +377,7 @@ class MethodDefinition:
parameter_map = dict(sig.parameters)
parameter_map.pop("cc", None)
code = " inputs = parse_command_input(\n"
code = " inputs = parse_command_inputs(\n"
for name, param in parameter_map.items():
if name == "extra_params":
code += f" {name}=kwargs,\n"
@@ -390,7 +390,7 @@ class MethodDefinition:
code += "},\n"
else:
code += f" {name}={name},\n"
code += " )\n"
code += " )\n\n"
code += " o = self._command_runner_session.run(\n"
code += f""" "{path}",\n"""
code += " **inputs,\n"

View File

@@ -0,0 +1,23 @@
import builtins
import warnings
from openbb_sdk_core.app.model.abstract.warning import OpenBBWarning
from openbb_sdk_core.app.model.command_output import CommandOutput
def parse_command_inputs(**kwargs) -> dict:
"""Parse and edit the inputs before running the command"""
return kwargs
def parse_command_output(command_output: CommandOutput) -> CommandOutput:
"""Parse command output"""
if command_output.warnings:
for w in command_output.warnings:
category = getattr(builtins, w.category, OpenBBWarning)
warnings.warn(w.message, category)
if command_output.error:
raise Exception(command_output.error.message)
return command_output

View File

@@ -1,45 +0,0 @@
import builtins
import warnings
from openbb_sdk_core.app.model.abstract.warning import OpenBBWarning
from openbb_sdk_core.app.model.command_output import CommandOutput
def parse_command_input(**kwargs) -> dict:
# Here we can inject provider defaults
# Covert data from pandas.DataFrame to BaseModel
# And other input parsing stuff
if "provider_choices" in kwargs:
if kwargs["provider_choices"].get("provider", None) is None:
raise NotImplementedError(
"'provider' cannot be None, defaults are not implemented yet."
)
return kwargs
# POC
# if "data" in parameter_map:
# code += " if isinstance(data, pandas.DataFrame):\n"
# code += " data = df_to_basemodel(data, data.index.name is not None)\n"
# code += "\n"
# if "provider_choices" in parameter_map:
# code += " if provider is None:\n"
# code += " defaults = self._command_runner_session.user_settings.defaults.endpoints.get(\n"
# code += f' "{path}",\n'
# code += " None,\n"
# code += " )\n"
# code += ' provider = defaults.get("provider", None) if defaults else Parameter.empty\n'
# code += "\n"
def parse_command_output(command_output: CommandOutput) -> CommandOutput:
if command_output.warnings:
for w in command_output.warnings:
category = getattr(builtins, w.category, OpenBBWarning)
warnings.warn(w.message, category)
if command_output.error:
raise Exception(command_output.error.message)
return command_output

View File

@@ -3,5 +3,18 @@
"benzinga_api_key": null,
"fmp_api_key": null,
"polygon_api_key": null
},
"defaults": {
"routes": {
"/stocks/fa/balance": {
"provider": "polygon"
},
"/stocks/load": {
"provider": "fmp"
},
"/stocks/news": {
"provider": "benzinga"
}
}
}
}