Skip to content

Commit

Permalink
[Feature] Store defaults in hub account (#6491)
Browse files Browse the repository at this point in the history
* feat: store defaults in hub account

* fix: rename var

* feat: v3Credentials -> deprecatedCredentials

* feat: allow unmapped deprecations

* fix: account.save()

* fix: unit tests

* fix: update fallback message
  • Loading branch information
montezdesousa authored Jun 18, 2024
1 parent fdfacc8 commit d465f22
Show file tree
Hide file tree
Showing 18 changed files with 82 additions and 58 deletions.
48 changes: 24 additions & 24 deletions assets/extensions/provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"credentials": [
"alpha_vantage_api_key"
],
"v3Credentials": [
"API_KEY_ALPHAVANTAGE"
],
"deprecatedCredentials": {
"API_KEY_ALPHAVANTAGE": "alpha_vantage_api_key"
},
"website": "https://www.alphavantage.co",
"instructions": "Go to: https://www.alphavantage.co/support/#api-key\n\n![AlphaVantage](https://user-images.githubusercontent.com/46355364/207820936-46c2ba00-81ff-4cd3-98a4-4fa44412996f.png)\n\nFill out the form, pass Captcha, and click on, \"GET FREE API KEY\"."
},
Expand All @@ -31,9 +31,9 @@
"credentials": [
"biztoc_api_key"
],
"v3Credentials": [
"API_BIZTOC_TOKEN"
],
"deprecatedCredentials": {
"API_BIZTOC_TOKEN": "biztoc_api_key"
},
"website": "https://api.biztoc.com",
"instructions": "The BizToc API is hosted on RapidAPI. To set up, go to: https://rapidapi.com/thma/api/biztoc.\n\n![biztoc0](https://github.com/marban/OpenBBTerminal/assets/18151143/04cdd423-f65e-4ad8-ad5a-4a59b0f5ddda)\n\nIn the top right, select 'Sign Up'. After answering some questions, you will be prompted to select one of their plans.\n\n![biztoc1](https://github.com/marban/OpenBBTerminal/assets/18151143/9f3b72ea-ded7-48c5-aa33-bec5c0de8422)\n\nAfter signing up, navigate back to https://rapidapi.com/thma/api/biztoc. If you are logged in, you will see a header called X-RapidAPI-Key.\n\n![biztoc2](https://github.com/marban/OpenBBTerminal/assets/18151143/0f3b6c91-07e0-447a-90cd-a9e23522929f)"
},
Expand Down Expand Up @@ -95,9 +95,9 @@
"credentials": [
"fmp_api_key"
],
"v3Credentials": [
"API_KEY_FINANCIALMODELINGPREP"
],
"deprecatedCredentials": {
"API_KEY_FINANCIALMODELINGPREP": "fmp_api_key"
},
"website": "https://financialmodelingprep.com",
"instructions": "Go to: https://site.financialmodelingprep.com/developer/docs\n\n![FinancialModelingPrep](https://user-images.githubusercontent.com/46355364/207821920-64553d05-d461-4984-b0fe-be0368c71186.png)\n\nClick on, \"Get my API KEY here\", and sign up for a free account.\n\n![FinancialModelingPrep](https://user-images.githubusercontent.com/46355364/207822184-a723092e-ef42-4f87-8c55-db150f09741b.png)\n\nWith an account created, sign in and navigate to the Dashboard, which shows the assigned token. by pressing the \"Dashboard\" button which will show the API key.\n\n![FinancialModelingPrep](https://user-images.githubusercontent.com/46355364/207823170-dd8191db-e125-44e5-b4f3-2df0e115c91d.png)"
},
Expand All @@ -109,9 +109,9 @@
"credentials": [
"fred_api_key"
],
"v3Credentials": [
"API_FRED_KEY"
],
"deprecatedCredentials": {
"API_FRED_KEY": "fred_api_key"
},
"website": "https://fred.stlouisfed.org",
"instructions": "Go to: https://fred.stlouisfed.org\n\n![FRED](https://user-images.githubusercontent.com/46355364/207827137-d143ba4c-72cb-467d-a7f4-5cc27c597aec.png)\n\nClick on, \"My Account\", create a new account or sign in with Google:\n\n![FRED](https://user-images.githubusercontent.com/46355364/207827011-65cdd501-27e3-436f-bd9d-b0d8381d46a7.png)\n\nAfter completing the sign-up, go to \"My Account\", and select \"API Keys\". Then, click on, \"Request API Key\".\n\n![FRED](https://user-images.githubusercontent.com/46355364/207827577-c869f989-4ef4-4949-ab57-6f3931f2ae9d.png)\n\nFill in the box for information about the use-case for FRED, and by clicking, \"Request API key\", at the bottom of the page, the API key will be issued.\n\n![FRED](https://user-images.githubusercontent.com/46355364/207828032-0a32d3b8-1378-4db2-9064-aa1eb2111632.png)"
},
Expand All @@ -131,9 +131,9 @@
"credentials": [
"intrinio_api_key"
],
"v3Credentials": [
"API_INTRINIO_KEY"
],
"deprecatedCredentials": {
"API_INTRINIO_KEY": "intrinio_api_key"
},
"website": "https://intrinio.com",
"instructions": "Go to: https://intrinio.com/starter-plan\n\n![Intrinio](https://user-images.githubusercontent.com/85772166/219207556-fcfee614-59f1-46ae-bff4-c63dd2f6991d.png)\n\nAn API key will be issued with a subscription. Find the token value within the account dashboard."
},
Expand All @@ -145,9 +145,9 @@
"credentials": [
"nasdaq_api_key"
],
"v3Credentials": [
"API_KEY_QUANDL"
],
"deprecatedCredentials": {
"API_KEY_QUANDL": "nasdaq_api_key"
},
"website": "https://data.nasdaq.com",
"instructions": "Go to: https://www.quandl.com\n\n![Quandl](https://user-images.githubusercontent.com/46355364/207823899-208a3952-f557-4b73-aee6-64ac00faedb7.png)\n\nClick on, \"Sign Up\", and register a new account.\n\n![Quandl](https://user-images.githubusercontent.com/46355364/207824214-4b6b2b74-e709-4ed4-adf2-14803e6f3568.png)\n\nFollow the sign-up instructions, and upon completion the API key will be assigned.\n\n![Quandl](https://user-images.githubusercontent.com/46355364/207824664-3c82befb-9c69-42df-8a82-510d85c19a97.png)"
},
Expand All @@ -167,9 +167,9 @@
"credentials": [
"polygon_api_key"
],
"v3Credentials": [
"API_POLYGON_KEY"
],
"deprecatedCredentials": {
"API_POLYGON_KEY": "polygon_api_key"
},
"website": "https://polygon.io",
"instructions": "Go to: https://polygon.io\n\n![Polygon](https://user-images.githubusercontent.com/46355364/207825623-fcd7f0a3-131a-4294-808c-754c13e38e2a.png)\n\nClick on, \"Get your Free API Key\".\n\n![Polygon](https://user-images.githubusercontent.com/46355364/207825952-ca5540ec-6ed2-4cef-a0ed-bb50b813932c.png)\n\nAfter signing up, the API Key is found at the bottom of the account dashboard page.\n\n![Polygon](https://user-images.githubusercontent.com/46355364/207826258-b1f318fa-fd9c-41d9-bf5c-fe16722e6601.png)"
},
Expand Down Expand Up @@ -224,9 +224,9 @@
"tradier_api_key",
"tradier_account_type"
],
"v3Credentials": [
"API_TRADIER_TOKEN"
],
"deprecatedCredentials": {
"API_TRADIER_TOKEN": "tradier_api_key"
},
"website": "https://tradier.com",
"instructions": "Go to: https://documentation.tradier.com\n\n![Tradier](https://user-images.githubusercontent.com/46355364/207829178-a8bba770-f2ea-4480-b28e-efd81cf30980.png)\n\nClick on, \"Open Account\", to start the sign-up process. After the account has been setup, navigate to [Tradier Broker Dash](https://dash.tradier.com/login?redirect=settings.api) and create the application. Request a sandbox access token."
},
Expand Down
10 changes: 5 additions & 5 deletions assets/scripts/generate_extension_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def to_camel(string: str):
return components[0] + "".join(x.title() for x in components[1:])


def createItem(package_name: str, obj: object, obj_attrs: List[str]) -> Dict[str, Any]:
def create_item(package_name: str, obj: object, obj_attrs: List[str]) -> Dict[str, Any]:
"""Create dictionary item from object attributes."""
pkg_spec = OPENBB_PLATFORM_TOML.data["tool"]["poetry"]["dependencies"].get(
package_name
Expand All @@ -68,7 +68,7 @@ def generate_provider_extensions() -> None:
"repr_name",
"description",
"credentials",
"v3_credentials",
"deprecated_credentials",
"website",
"instructions",
]
Expand All @@ -80,7 +80,7 @@ def generate_provider_extensions() -> None:
file, obj = file_obj[0], file_obj[1]
module = import_module(file)
provider_obj = getattr(module, obj)
data.append(createItem(pkg_name, provider_obj, obj_attrs))
data.append(create_item(pkg_name, provider_obj, obj_attrs))
write("provider", data)


Expand All @@ -96,7 +96,7 @@ def generate_router_extensions() -> None:
file, obj = file_obj[0], file_obj[1]
module = import_module(file)
router_obj = getattr(module, obj)
data.append(createItem(pkg_name, router_obj, obj_attrs))
data.append(create_item(pkg_name, router_obj, obj_attrs))
write("router", data)


Expand All @@ -112,7 +112,7 @@ def generate_obbject_extensions() -> None:
file, obj = file_obj[0], file_obj[1]
module = import_module(file)
ext_obj = getattr(module, obj)
data.append(createItem(pkg_name, ext_obj, obj_attrs))
data.append(create_item(pkg_name, ext_obj, obj_attrs))
write("obbject", data)


Expand Down
5 changes: 5 additions & 0 deletions openbb_platform/core/openbb_core/app/model/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ def validate_before(cls, values: dict) -> dict:
v["provider"] = [provider]
new_values["commands"][clean_k] = v
return new_values

def update(self, incoming: "Defaults"):
"""Update current defaults."""
incoming_commands = incoming.model_dump(exclude_none=True).get("commands", {})
self.__dict__["commands"].update(incoming_commands)
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""Hub user settings model."""

from typing import Dict, Optional
from typing import Any, Dict, Optional

from pydantic import BaseModel, ConfigDict, Field


class HubUserSettings(BaseModel):
"""Hub user settings model."""

# features_settings: Dict[str, str]
features_settings: Dict[str, Any] = Field(default_factory=dict)
features_keys: Dict[str, Optional[str]] = Field(default_factory=dict)
# features_sources: Dict[str, List[str]]
# features_sources: Dict[str, Any]
# features_terminal_style: Dict[str, Union[str, Dict[str, str]]]

model_config = ConfigDict(validate_assignment=True)
24 changes: 17 additions & 7 deletions openbb_platform/core/openbb_core/app/service/hub_service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Hub manager class."""

from typing import Optional
from typing import Optional, Tuple
from warnings import warn

from fastapi import HTTPException
from jwt import ExpiredSignatureError, PyJWTError, decode, get_unverified_header
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.credentials import Credentials
from openbb_core.app.model.defaults import Defaults
from openbb_core.app.model.hub.hub_session import HubSession
from openbb_core.app.model.hub.hub_user_settings import HubUserSettings
from openbb_core.app.model.profile import Profile
Expand Down Expand Up @@ -81,7 +82,9 @@ def push(self, user_settings: UserSettings) -> bool:
"""Push user settings to Hub."""
if self._session:
if user_settings.credentials:
hub_user_settings = self.platform2hub(user_settings.credentials)
hub_user_settings = self.platform2hub(
user_settings.credentials, user_settings.defaults
)
return self._put_user_settings(self._session, hub_user_settings)
return False
raise OpenBBError(
Expand All @@ -93,8 +96,10 @@ def pull(self) -> UserSettings:
if self._session:
self._hub_user_settings = self._get_user_settings(self._session)
profile = Profile(hub_session=self._session)
credentials = self.hub2platform(self._hub_user_settings)
return UserSettings(profile=profile, credentials=credentials)
credentials, defaults = self.hub2platform(self._hub_user_settings)
return UserSettings(
profile=profile, credentials=credentials, defaults=defaults
)
raise OpenBBError(
"No session found. Login or provide a 'HubSession' on initialization."
)
Expand Down Expand Up @@ -223,7 +228,7 @@ def _put_user_settings(
detail = response.json().get("detail", None)
raise HTTPException(status_code, detail)

def hub2platform(self, settings: HubUserSettings) -> Credentials:
def hub2platform(self, settings: HubUserSettings) -> Tuple[Credentials, Defaults]:
"""Convert Hub user settings to Platform models."""
if any(k in settings.features_keys for k in self.V3TOV4):
deprecated = {
Expand All @@ -242,9 +247,12 @@ def hub2platform(self, settings: HubUserSettings) -> Credentials:
self.V3TOV4.get(k, k): settings.features_keys.get(self.V3TOV4.get(k, k), v)
for k, v in settings.features_keys.items()
}
return Credentials(**hub_credentials)
defaults = settings.features_settings.get("defaults", {})
return Credentials(**hub_credentials), Defaults(**defaults)

def platform2hub(self, credentials: Credentials) -> HubUserSettings:
def platform2hub(
self, credentials: Credentials, defaults: Defaults
) -> HubUserSettings:
"""Convert Platform models to Hub user settings."""
# Dump mode json ensures SecretStr values are serialized as strings
credentials = credentials.model_dump(mode="json", exclude_none=True)
Expand All @@ -254,6 +262,8 @@ def platform2hub(self, credentials: Credentials) -> HubUserSettings:
# If v3 key was in the hub already, we keep it
k = v3_k if v3_k in settings.features_keys else v4_k
settings.features_keys[k] = v
defaults_ = defaults.model_dump(mode="json", exclude_none=True)
settings.features_settings.update({"defaults": defaults_})
return settings

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions openbb_platform/core/openbb_core/app/static/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def login(
incoming = self._hub_service.pull()
self._base_app.user.profile = incoming.profile
self._base_app.user.credentials.update(incoming.credentials)
self._base_app.user.defaults.update(incoming.defaults)
if remember_me:
Path(self._openbb_directory).mkdir(parents=False, exist_ok=True)
session_file = Path(self._openbb_directory, self.SESSION_FILE)
Expand Down Expand Up @@ -187,6 +188,7 @@ def refresh(self, return_settings: bool = False) -> Optional[UserSettings]:
incoming = self._hub_service.pull()
self._base_app.user.profile = incoming.profile
self._base_app.user.credentials.update(incoming.credentials)
self._base_app.user.defaults.update(incoming.defaults)
if return_settings:
return self._base_app._command_runner.user_settings
return None
Expand Down
4 changes: 2 additions & 2 deletions openbb_platform/core/openbb_core/app/static/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ def _get_provider(
result = self._check_credentials(p)
if result:
return p
elif result is False:
if result is False:
tries.append((p, "missing credentials"))
else:
tries.append((p, "not found"))

msg = "\n ".join([f"* '{pair[0]}' -> {pair[1]}" for pair in tries])
raise OpenBBError(
f"Provider fallback failed, please specify the provider or update credentials.\n"
f"Provider fallback failed, please specify one of the available providers or update credentials.\n"
f"[Providers]\n {msg}"
)
return choice
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(
credentials: Optional[List[str]] = None,
fetcher_dict: Optional[Dict[str, Type[Fetcher]]] = None,
repr_name: Optional[str] = None,
v3_credentials: Optional[List[str]] = None,
deprecated_credentials: Optional[Dict[str, Optional[str]]] = None,
instructions: Optional[str] = None,
) -> None:
"""Initialize the provider.
Expand All @@ -36,8 +36,8 @@ def __init__(
Dictionary of fetchers, by default None.
repr_name: Optional[str]
Full name of the provider, by default None.
v3_credentials: Optional[List[str]]
List of corresponding v3 credentials, by default None.
deprecated_credentials: Optional[Dict[str, Optional[str]]]
Map of deprecated credentials to its current name, by default None.
instructions: Optional[str]
Instructions on how to setup the provider. For example, how to get an API key.
"""
Expand All @@ -52,5 +52,5 @@ def __init__(
for c in credentials:
self.credentials.append(f"{self.name.lower()}_{c}")
self.repr_name = repr_name
self.v3_credentials = v3_credentials
self.deprecated_credentials = deprecated_credentials
self.instructions = instructions
15 changes: 11 additions & 4 deletions openbb_platform/core/tests/app/service/test_hub_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import pytest
from jwt import encode
from openbb_core.app.model.defaults import Defaults
from openbb_core.app.service.hub_service import (
Credentials,
HubService,
Expand Down Expand Up @@ -242,8 +243,9 @@ def test_hub2platform_v4_only():
"polygon_api_key": "def",
"fred_api_key": "ghi",
}
mock_user_settings.features_settings = {}

credentials = HubService().hub2platform(mock_user_settings)
credentials, _ = HubService().hub2platform(mock_user_settings)
assert isinstance(credentials, Credentials)
assert credentials.fmp_api_key.get_secret_value() == "abc"
assert credentials.polygon_api_key.get_secret_value() == "def"
Expand All @@ -258,8 +260,9 @@ def test_hub2platform_v3_only():
"API_POLYGON_KEY": "def",
"API_FRED_KEY": "ghi",
}
mock_user_settings.features_settings = {}

credentials = HubService().hub2platform(mock_user_settings)
credentials, _ = HubService().hub2platform(mock_user_settings)
assert isinstance(credentials, Credentials)
assert credentials.fmp_api_key.get_secret_value() == "abc"
assert credentials.polygon_api_key.get_secret_value() == "def"
Expand All @@ -275,8 +278,9 @@ def test_hub2platform_v3v4():
"API_POLYGON_KEY": "def",
"API_FRED_KEY": "ghi",
}
mock_user_settings.features_settings = {}

credentials = HubService().hub2platform(mock_user_settings)
credentials, _ = HubService().hub2platform(mock_user_settings)
assert isinstance(credentials, Credentials)
assert credentials.fmp_api_key.get_secret_value() == "other_key"
assert credentials.polygon_api_key.get_secret_value() == "def"
Expand All @@ -291,6 +295,7 @@ def test_platform2hub():
"fmp_api_key": "other_key",
"API_FRED_KEY": "ghi",
}
mock_user_settings.features_settings = {}
mock_hub_service = HubService()
mock_hub_service._hub_user_settings = mock_user_settings
mock_credentials = Credentials( # Current credentials
Expand All @@ -300,7 +305,8 @@ def test_platform2hub():
benzinga_api_key=SecretStr("benzinga"),
some_api_key=SecretStr("some"),
)
user_settings = mock_hub_service.platform2hub(mock_credentials)
mock_defaults = Defaults()
user_settings = mock_hub_service.platform2hub(mock_credentials, mock_defaults)

assert isinstance(user_settings, HubUserSettings)
assert user_settings.features_keys["API_KEY_FINANCIALMODELINGPREP"] == "fmp"
Expand All @@ -309,6 +315,7 @@ def test_platform2hub():
assert user_settings.features_keys["API_FRED_KEY"] == "fred"
assert user_settings.features_keys["benzinga_api_key"] == "benzinga"
assert "some_api_key" not in user_settings.features_keys
assert "defaults" in user_settings.features_settings


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion openbb_platform/core/tests/app/static/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def test_container__check_credentials(container):
("provider_1", "provider_2"),
OpenBBError,
escape(
"Provider fallback failed, please specify the provider or update credentials."
"Provider fallback failed, please specify one of the available providers or update credentials."
"\n[Providers]\n * 'x' -> not found\n * 'y' -> not found\n * 'z' -> not found"
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
"EtfHistorical": AVEquityHistoricalFetcher,
},
repr_name="Alpha Vantage",
v3_credentials=["API_KEY_ALPHAVANTAGE"],
deprecated_credentials={"API_KEY_ALPHAVANTAGE": "alpha_vantage_api_key"},
instructions='Go to: https://www.alphavantage.co/support/#api-key\n\n![AlphaVantage](https://user-images.githubusercontent.com/46355364/207820936-46c2ba00-81ff-4cd3-98a4-4fa44412996f.png)\n\nFill out the form, pass Captcha, and click on, "GET FREE API KEY".', # noqa: E501 pylint: disable=line-too-long
)
Loading

0 comments on commit d465f22

Please sign in to comment.