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

OBBject extensions #5612

Merged
merged 30 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
64379cf
changes to core
montezdesousa Oct 26, 2023
df2cd4c
bug?
montezdesousa Oct 26, 2023
bb4a7d4
docstring
montezdesousa Oct 26, 2023
eb2b479
doc
montezdesousa Oct 26, 2023
f2d5b73
Update credentials.py
montezdesousa Oct 26, 2023
f6bd6cb
fix model_dump
montezdesousa Oct 27, 2023
862ae60
create extensions
montezdesousa Oct 27, 2023
b059cb3
fix docstring
montezdesousa Oct 27, 2023
31710da
doc
montezdesousa Oct 27, 2023
81c9c47
revert change query_exc
montezdesousa Oct 27, 2023
5599d53
doc
montezdesousa Oct 27, 2023
14aacbc
fix container test
montezdesousa Oct 30, 2023
b27e171
redirect obbject test patch
montezdesousa Oct 30, 2023
4ef513c
doc
montezdesousa Oct 30, 2023
7d78cbd
rename method
montezdesousa Oct 30, 2023
84a92e8
move decorator to extension file
montezdesousa Oct 30, 2023
f665761
rename method extend_obbject
montezdesousa Oct 30, 2023
a68c870
changes in creds model
montezdesousa Oct 30, 2023
1b3be48
avoid credential racing
montezdesousa Oct 30, 2023
bd0da75
doc
montezdesousa Oct 30, 2023
7813fd4
rename prop
montezdesousa Oct 30, 2023
437c993
doc
montezdesousa Oct 30, 2023
c3f2968
doc
montezdesousa Oct 30, 2023
3d68fb3
remove comment
montezdesousa Oct 30, 2023
75ef2a7
comment some code
montezdesousa Oct 30, 2023
9a5a196
free extension names
montezdesousa Oct 30, 2023
0cbdbb5
docstring
montezdesousa Oct 30, 2023
5ff9ab7
doc
montezdesousa Oct 30, 2023
f5dac6b
Merge branch 'feature/openbb-sdk-v4' into feature/obbject_extensions
montezdesousa Oct 30, 2023
41d0fa0
docs
montezdesousa Oct 30, 2023
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
72 changes: 56 additions & 16 deletions openbb_platform/platform/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
- [4.1 Static version](#41-static-version)
- [4.1.1. OBBject](#411-obbject)
- [Helpers](#helpers)
- [Extensions](#extensions)
- [4.1.2. Utilities](#412-utilities)
- [User settings](#user-settings)
- [Preferences](#preferences)
- [System settings](#system-settings)
- [Preferences](#preferences)
- [Available preferences and its descriptions](#available-preferences-and-its-descriptions)
- [Coverage](#coverage)
- [4.1.3. OpenBB Hub Account](#413-openbb-hub-account)
- [4.1.4. Command execution](#414-command-execution)
Expand Down Expand Up @@ -63,7 +63,7 @@ poetry install
Build a Python package:

```bash
poetry new openbb-sdk-my_extension
poetry new openbb-platform-my_extension
```

### Command
Expand Down Expand Up @@ -235,11 +235,52 @@ date
}
```

#### Extensions

Steps to create an `OBBject` extension:

1. Set the following as entry point in your extension .toml file and install it:

```toml
...
[tool.poetry.plugins."openbb_obbject_extension"]
example = "openbb_example:ext"
```

2. Extension code:

```python
from openbb_core.app.model.extension import Extension
ext = Extension(name="example", required_credentials=["some_api_key"])
```

3. Optionally declare an `OBBject` accessor, it will use the extension name:

```python
@ext.obbject_accessor
class Example:
def __init__(self, obbject):
self._obbject = obbject

def hello(self):
api_key = self._obbject._credentials.some_api_key
print(f"Hello, this is my credential: {api_key}!")
```

Usage:

```shell
>>> from openbb import obb
>>> obbject = obb.stock.load("AAPL")
>>> obbject.example.hello()
Hello, this is my credential: None!
```

### 4.1.2. Utilities

#### User settings

These are your user settings, you can change them anytime and they will be applied. Don't forget to `sdk.account.save()` if you want these changes to persist.
These are your user settings, you can change them anytime and they will be applied. Don't forget to `obb.account.save()` if you want these changes to persist.

```python
from openbb import obb
Expand All @@ -250,16 +291,6 @@ obb.user.preferences
obb.user.defaults
```

#### System settings

Check your system settings.

```python
from openbb import obb

obb.system
```

#### Preferences

Check your preferences by adjusting the `user_settings.json` file inside your **home** directory.
Expand All @@ -282,7 +313,7 @@ Here is an example of how your `user_settings.json` file can look like:

> Note that the user preferences shouldn't be confused with environment variables.

##### Available preferences and its descriptions
These are the available preferences and respective descriptions:

|Preference |Default |Description |
|---------------------|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand All @@ -300,6 +331,15 @@ Here is an example of how your `user_settings.json` file can look like:
|metadata |True |Enables or disables the collection of metadata which provides information about operations including arguments duration route and timestamp. Disabling this feature may improve performance in cases where contextual information is not needed or when the additional computation time and storage space are a concern.|
|output_type |OBBject |Specifies the type of data the application will output when a command or endpoint is accessed. Note that choosing data formats only available in Python such as `dataframe`, `numpy` or `polars` will render the application's API non-functional. |

#### System settings

Check your system settings.

```python
from openbb import obb

obb.system
```

#### Coverage

Expand Down Expand Up @@ -389,7 +429,7 @@ To apply an environment variable use one of the following:
```python
import os
os.environ["OPENBB_DEBUG_MODE"] = "True"
from openbb import sdk
from openbb import obb
```

2. Persistent: create a `.env` file in `/.openbb_platform` folder inside your home directory with
Expand Down
128 changes: 90 additions & 38 deletions openbb_platform/platform/core/openbb_core/app/model/credentials.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,103 @@
from typing import Dict, List, Optional, Tuple

from pydantic import ConfigDict, SecretStr, create_model, field_serializer
import traceback
from typing import Dict, Optional, Set, Tuple

from importlib_metadata import entry_points
from pydantic import (
BaseModel,
ConfigDict,
Field,
SecretStr,
create_model,
)
from pydantic.functional_serializers import PlainSerializer
from typing_extensions import Annotated

from openbb_core.app.model.extension import Extension
from openbb_core.app.provider_interface import ProviderInterface

# Here we create the BaseModel from the provider required credentials.
# This means that if a new provider extension is installed, the required
# credentials will be automatically added to the Credentials model.


def format_map(
required_credentials: List[str],
) -> Dict[str, Tuple[object, None]]:
"""Format credentials map to be used in the Credentials model"""
formatted: Dict[str, Tuple[object, None]] = {}
for c in required_credentials:
formatted[c] = (Optional[SecretStr], None)

return formatted

class LoadingError(Exception):
"""Error loading extension."""


# @model_serializer blocks model_dump with pydantic parameters (include, exclude)
OBBSecretStr = Annotated[
SecretStr,
PlainSerializer(
lambda x: x.get_secret_value(), return_type=str, when_used="json-unless-none"
),
]


class CredentialsLoader:
"""Here we create the Credentials model"""

credentials: Dict[str, Set[str]] = {}

@staticmethod
def prepare(
required_credentials: Dict[str, Set[str]],
) -> Dict[str, Tuple[object, None]]:
"""Prepare credentials map to be used in the Credentials model"""
formatted: Dict[str, Tuple[object, None]] = {}
for origin, creds in required_credentials.items():
for c in creds:
# Not sure we should do this, if you require the same credential it breaks
# if c in formatted:
# raise ValueError(f"Credential '{c}' already in use.")
formatted[c] = (
Optional[OBBSecretStr],
Field(
default=None, description=origin
), # register the credential origin (obbject, providers)
)

return formatted

def from_obbject(self) -> None:
"""Load credentials from OBBject extensions"""
self.credentials["obbject"] = set()
for entry_point in sorted(entry_points(group="openbb_obbject_extension")):
try:
entry = entry_point.load()
if isinstance(entry, Extension):
for c in entry.required_credentials:
self.credentials["obbject"].add(c)
except Exception as e:
traceback.print_exception(type(e), e, e.__traceback__)
raise LoadingError(f"Invalid extension '{entry_point.name}'") from e

def from_providers(self) -> None:
"""Load credentials from providers"""
self.credentials["providers"] = set()
for c in ProviderInterface().required_credentials:
self.credentials["providers"].add(c)

def load(self) -> BaseModel:
"""Load credentials from providers"""
# We load providers first to give them priority choosing credential names
self.from_providers()
self.from_obbject()
return create_model( # type: ignore
"Credentials",
__config__=ConfigDict(validate_assignment=True),
**self.prepare(self.credentials),
)

provider_credentials = ProviderInterface().required_credentials

_Credentials = create_model( # type: ignore
"Credentials",
__config__=ConfigDict(validate_assignment=True),
**format_map(provider_credentials),
)
_Credentials = CredentialsLoader().load()


class Credentials(_Credentials):
class Credentials(_Credentials): # type: ignore
"""Credentials model used to store provider credentials"""

@field_serializer(*provider_credentials, when_used="json-unless-none")
def _dump_secret(self, v):
return v.get_secret_value()
def __repr__(self) -> str:
"""String representation of the credentials"""
return (
self.__class__.__name__
+ "\n\n"
+ "\n".join([f"{k}: {v}" for k, v in self.__dict__.items()])
)

def show(self):
"""Unmask credentials and print them"""
Expand All @@ -43,14 +106,3 @@ def show(self):
+ "\n\n"
+ "\n".join([f"{k}: {v}" for k, v in self.model_dump(mode="json").items()])
)


def __repr__(self: Credentials) -> str:
return (
self.__class__.__name__
+ "\n\n"
+ "\n".join([f"{k}: {v}" for k, v in self.model_dump().items()])
)


Credentials.__repr__ = __repr__
70 changes: 70 additions & 0 deletions openbb_platform/platform/core/openbb_core/app/model/extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import warnings
from typing import Callable, List, Optional


class Extension:
"""Serves as extension entry point and must be created by each extension package.

See README.md for more information on how to create an extension.
"""

def __init__(
self,
name: str,
required_credentials: Optional[List[str]] = None,
) -> None:
"""Initialize the extension.

Parameters
----------
name : str
Name of the extension.
required_credentials : Optional[List[str]], optional
List of required credentials, by default None
"""
self.name = name
self.required_credentials = required_credentials or []

@property
def obbject_accessor(self) -> Callable:
"""Extend an OBBject, inspired by pandas."""
# pylint: disable=import-outside-toplevel
# Avoid circular imports

from openbb_core.app.model.obbject import OBBject

return self.register_accessor(self.name, OBBject)

@staticmethod
def register_accessor(name, cls) -> Callable:
"""Register a custom accessor"""

def decorator(accessor):
if hasattr(cls, name):
warnings.warn(
f"registration of accessor '{repr(accessor)}' under name "
f"'{repr(name)}' for type '{repr(cls)}' is overriding a preexisting "
f"attribute with the same name.",
UserWarning,
)
setattr(cls, name, CachedAccessor(name, accessor))
# pylint: disable=protected-access
cls._accessors.add(name)
return accessor

return decorator


class CachedAccessor:
"""CachedAccessor"""

def __init__(self, name: str, accessor) -> None:
self._name = name
self._accessor = accessor

def __get__(self, obj, cls):
if obj is None:
return self._accessor
accessor_obj = self._accessor(obj)
object.__setattr__(obj, self._name, accessor_obj)
return accessor_obj
19 changes: 17 additions & 2 deletions openbb_platform/platform/core/openbb_core/app/model/obbject.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
"""The OBBject."""
from re import sub
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Literal, Optional, TypeVar
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
Generic,
List,
Literal,
Optional,
Set,
TypeVar,
)

import pandas as pd
from numpy import ndarray
from pydantic import BaseModel, Field

from openbb_core.app.charting_service import ChartingService
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.abstract.tagged import Tagged
from openbb_core.app.model.abstract.warning import Warning_
Expand Down Expand Up @@ -47,6 +57,8 @@ class OBBject(Tagged, Generic[T]):
default_factory=dict,
description="Extra info.",
)
_credentials: ClassVar[Optional[BaseModel]] = None
_accessors: ClassVar[Set[str]] = set()

def __repr__(self) -> str:
"""Human readable representation of the object."""
Expand Down Expand Up @@ -236,6 +248,9 @@ def to_chart(self, **kwargs):
chart.fig
The chart figure.
"""
# pylint: disable=import-outside-toplevel
# Avoids circular import
from openbb_core.app.charting_service import ChartingService

cs = ChartingService()
kwargs["data"] = self.to_dataframe()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class UserSettings(Tagged):
defaults: Defaults = Field(default_factory=Defaults)

def __repr__(self) -> str:
# We use the __dict__ because Credentials.model_dump() will use the serializer
# and unmask the credentials
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
f"{k}: {v}" for k, v in self.__dict__.items()
)
Loading
Loading