Upload 355 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- openbb_platform/core/README.md +57 -0
- openbb_platform/core/__init__.py +1 -0
- openbb_platform/core/integration/test_obbject.py +96 -0
- openbb_platform/core/openbb_core/__init__.py +1 -0
- openbb_platform/core/openbb_core/api/app_loader.py +45 -0
- openbb_platform/core/openbb_core/api/auth/user.py +57 -0
- openbb_platform/core/openbb_core/api/dependency/__init__.py +1 -0
- openbb_platform/core/openbb_core/api/dependency/coverage.py +21 -0
- openbb_platform/core/openbb_core/api/dependency/system.py +20 -0
- openbb_platform/core/openbb_core/api/exception_handlers.py +134 -0
- openbb_platform/core/openbb_core/api/rest_api.py +105 -0
- openbb_platform/core/openbb_core/api/router/__init__.py +1 -0
- openbb_platform/core/openbb_core/api/router/commands.py +266 -0
- openbb_platform/core/openbb_core/api/router/coverage.py +105 -0
- openbb_platform/core/openbb_core/api/router/helpers/__init__.py +1 -0
- openbb_platform/core/openbb_core/api/router/helpers/coverage_helpers.py +120 -0
- openbb_platform/core/openbb_core/api/router/system.py +16 -0
- openbb_platform/core/openbb_core/api/router/user.py +18 -0
- openbb_platform/core/openbb_core/app/__init__.py +1 -0
- openbb_platform/core/openbb_core/app/command_runner.py +512 -0
- openbb_platform/core/openbb_core/app/constants.py +8 -0
- openbb_platform/core/openbb_core/app/deprecation.py +65 -0
- openbb_platform/core/openbb_core/app/extension_loader.py +177 -0
- openbb_platform/core/openbb_core/app/logs/formatters/formatter_with_exceptions.py +208 -0
- openbb_platform/core/openbb_core/app/logs/handlers/path_tracking_file_handler.py +86 -0
- openbb_platform/core/openbb_core/app/logs/handlers/posthog_handler.py +151 -0
- openbb_platform/core/openbb_core/app/logs/handlers_manager.py +89 -0
- openbb_platform/core/openbb_core/app/logs/logging_service.py +260 -0
- openbb_platform/core/openbb_core/app/logs/models/logging_settings.py +58 -0
- openbb_platform/core/openbb_core/app/logs/utils/expired_files.py +30 -0
- openbb_platform/core/openbb_core/app/logs/utils/utils.py +74 -0
- openbb_platform/core/openbb_core/app/model/__init__.py +1 -0
- openbb_platform/core/openbb_core/app/model/abstract/__init__.py +1 -0
- openbb_platform/core/openbb_core/app/model/abstract/error.py +12 -0
- openbb_platform/core/openbb_core/app/model/abstract/results.py +5 -0
- openbb_platform/core/openbb_core/app/model/abstract/singleton.py +20 -0
- openbb_platform/core/openbb_core/app/model/abstract/tagged.py +10 -0
- openbb_platform/core/openbb_core/app/model/abstract/warning.py +24 -0
- openbb_platform/core/openbb_core/app/model/api_settings.py +57 -0
- openbb_platform/core/openbb_core/app/model/charts/chart.py +30 -0
- openbb_platform/core/openbb_core/app/model/charts/charting_settings.py +62 -0
- openbb_platform/core/openbb_core/app/model/command_context.py +12 -0
- openbb_platform/core/openbb_core/app/model/credentials.py +146 -0
- openbb_platform/core/openbb_core/app/model/defaults.py +56 -0
- openbb_platform/core/openbb_core/app/model/example.py +229 -0
- openbb_platform/core/openbb_core/app/model/extension.py +79 -0
- openbb_platform/core/openbb_core/app/model/field.py +28 -0
- openbb_platform/core/openbb_core/app/model/hub/hub_session.py +27 -0
- openbb_platform/core/openbb_core/app/model/hub/hub_user_settings.py +22 -0
- openbb_platform/core/openbb_core/app/model/metadata.py +142 -0
openbb_platform/core/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenBB Platform - Core
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Core extension serves as the foundational component of the OpenBB Platform. It encapsulates essential functionalities and serves as an infrastructural base for other extensions. This extension is vital for maintaining the integrity and standardization of the platform.
|
| 6 |
+
|
| 7 |
+
## Key Features
|
| 8 |
+
|
| 9 |
+
- **Standardized Data Model** (`Data` Class): A flexible and dynamic Pydantic model capable of handling various data structures.
|
| 10 |
+
- **Standardized Query Params** (`QueryParams` Class): A Pydantic model for handling querying to different providers.
|
| 11 |
+
- **Dynamic Field Support**: Enables handling of undefined fields, providing versatility in data processing.
|
| 12 |
+
- **Robust Data Validation**: Utilizes Pydantic's validation features to ensure data integrity.
|
| 13 |
+
- **API Routing Mechanism** (`Router` Class): Simplifies the process of defining API routes and endpoints - out of the box Python and Web endpoints.
|
| 14 |
+
|
| 15 |
+
## Getting Started
|
| 16 |
+
|
| 17 |
+
### Prerequisites
|
| 18 |
+
|
| 19 |
+
- Python 3.9 or higher.
|
| 20 |
+
- Familiarity with FastAPI and Pydantic.
|
| 21 |
+
|
| 22 |
+
### Installation
|
| 23 |
+
|
| 24 |
+
Installing through pip:
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
pip install openbb-core
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
> Note that, the openbb-core is an infrastructural component of the OpenBB Platform. It is not intended to be used as a standalone package.
|
| 31 |
+
|
| 32 |
+
### Usage
|
| 33 |
+
|
| 34 |
+
The Core extension is used as the basis for building and integrating new data sources, providers, and extensions into the OpenBB Platform. It provides the necessary classes and structures for standardizing and handling data.
|
| 35 |
+
|
| 36 |
+
### Contributing
|
| 37 |
+
|
| 38 |
+
We welcome contributions! If you're looking to contribute, please:
|
| 39 |
+
|
| 40 |
+
- Follow the existing coding standards and conventions.
|
| 41 |
+
- Write clear, documented code.
|
| 42 |
+
- Ensure your code does not negatively impact performance.
|
| 43 |
+
- Test your contributions thoroughly.
|
| 44 |
+
|
| 45 |
+
Please refer to our [Contributing Guidelines](https://docs.openbb.co/platform/developer_guide/contributing).
|
| 46 |
+
|
| 47 |
+
### Collaboration
|
| 48 |
+
|
| 49 |
+
Engage with the development team and the community. Be open to feedback and collaborative discussions.
|
| 50 |
+
|
| 51 |
+
### Support
|
| 52 |
+
|
| 53 |
+
For support, questions, or more information, please visit [OpenBB Platform Documentation](https://docs.openbb.co/platform).
|
| 54 |
+
|
| 55 |
+
### License
|
| 56 |
+
|
| 57 |
+
This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/OpenBB-finance/OpenBB/blob/main/LICENSE) file for details.
|
openbb_platform/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core Module."""
|
openbb_platform/core/integration/test_obbject.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test the OBBject."""
|
| 2 |
+
|
| 3 |
+
import contextlib
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
with contextlib.suppress(ImportError):
|
| 9 |
+
import polars as pl
|
| 10 |
+
|
| 11 |
+
with contextlib.suppress(ImportError):
|
| 12 |
+
import pandas as pd
|
| 13 |
+
|
| 14 |
+
with contextlib.suppress(ImportError):
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
with contextlib.suppress(ImportError):
|
| 18 |
+
from openbb_charting.core.openbb_figure import OpenBBFigure
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# pylint: disable=inconsistent-return-statements
|
| 22 |
+
@pytest.fixture(scope="session")
|
| 23 |
+
def obb(pytestconfig):
|
| 24 |
+
"""Fixture to setup obb."""
|
| 25 |
+
|
| 26 |
+
if pytestconfig.getoption("markexpr") != "not integration":
|
| 27 |
+
import openbb # pylint: disable=import-outside-toplevel
|
| 28 |
+
|
| 29 |
+
return openbb.obb
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# pylint: disable=redefined-outer-name
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@pytest.mark.skipif("pandas" not in sys.modules, reason="pandas not installed")
|
| 36 |
+
@pytest.mark.integration
|
| 37 |
+
def test_to_dataframe(obb):
|
| 38 |
+
"""Test obbject to dataframe."""
|
| 39 |
+
|
| 40 |
+
stocks_df = obb.equity.price.historical("AAPL", provider="fmp").to_dataframe()
|
| 41 |
+
assert isinstance(stocks_df, pd.DataFrame)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@pytest.mark.skipif(
|
| 45 |
+
"polars" not in sys.modules or "polars-lts-cpu" not in sys.modules,
|
| 46 |
+
reason="polars not installed",
|
| 47 |
+
)
|
| 48 |
+
@pytest.mark.integration
|
| 49 |
+
def test_to_polars(obb):
|
| 50 |
+
"""Test obbject to polars."""
|
| 51 |
+
|
| 52 |
+
crypto_pl = obb.crypto.price.historical("BTC-USD", provider="fmp").to_polars()
|
| 53 |
+
assert isinstance(crypto_pl, pl.DataFrame)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@pytest.mark.skipif("numpy" not in sys.modules, reason="numpy not installed")
|
| 57 |
+
@pytest.mark.integration
|
| 58 |
+
def test_to_numpy(obb):
|
| 59 |
+
"""Test obbject to numpy array."""
|
| 60 |
+
|
| 61 |
+
cpi_np = obb.economy.cpi(
|
| 62 |
+
country=["portugal", "spain", "switzerland"], frequency="annual"
|
| 63 |
+
).to_numpy()
|
| 64 |
+
assert isinstance(cpi_np, np.ndarray)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@pytest.mark.integration
|
| 68 |
+
def test_to_dict(obb):
|
| 69 |
+
"""Test obbject to dict."""
|
| 70 |
+
|
| 71 |
+
fed_dict = obb.fixedincome.rate.ameribor(start_date="2020-01-01").to_dict()
|
| 72 |
+
assert isinstance(fed_dict, dict)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@pytest.mark.skipif(
|
| 76 |
+
"openbb_charting" not in sys.modules, reason="openbb_charting not installed"
|
| 77 |
+
)
|
| 78 |
+
@pytest.mark.integration
|
| 79 |
+
def test_to_chart(obb):
|
| 80 |
+
"""Test obbject to chart."""
|
| 81 |
+
|
| 82 |
+
res = obb.equity.price.historical("AAPL", provider="fmp")
|
| 83 |
+
res.charting.to_chart(render=False)
|
| 84 |
+
assert isinstance(res.chart.fig, OpenBBFigure)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@pytest.mark.skipif(
|
| 88 |
+
"openbb_charting" not in sys.modules, reason="openbb_charting not installed"
|
| 89 |
+
)
|
| 90 |
+
@pytest.mark.integration
|
| 91 |
+
def test_show(obb):
|
| 92 |
+
"""Test obbject to chart."""
|
| 93 |
+
|
| 94 |
+
stocks_data = obb.equity.price.historical("AAPL", provider="fmp", chart=True)
|
| 95 |
+
assert isinstance(stocks_data.chart.fig, OpenBBFigure)
|
| 96 |
+
assert stocks_data.chart.fig.show() is None
|
openbb_platform/core/openbb_core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core."""
|
openbb_platform/core/openbb_core/api/app_loader.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""App loader module."""
|
| 2 |
+
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, FastAPI
|
| 6 |
+
from fastapi.exceptions import ResponseValidationError
|
| 7 |
+
from openbb_core.api.exception_handlers import ExceptionHandlers
|
| 8 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 9 |
+
from openbb_core.app.router import RouterLoader
|
| 10 |
+
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
| 11 |
+
from pydantic import ValidationError
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AppLoader:
|
| 15 |
+
"""App loader."""
|
| 16 |
+
|
| 17 |
+
@staticmethod
|
| 18 |
+
def add_routers(app: FastAPI, routers: List[Optional[APIRouter]], prefix: str):
|
| 19 |
+
"""Add routers."""
|
| 20 |
+
for router in routers:
|
| 21 |
+
if router:
|
| 22 |
+
app.include_router(router=router, prefix=prefix)
|
| 23 |
+
|
| 24 |
+
@staticmethod
|
| 25 |
+
def add_openapi_tags(app: FastAPI):
|
| 26 |
+
"""Add openapi tags."""
|
| 27 |
+
main_router = RouterLoader.from_extensions()
|
| 28 |
+
# Add tag data for each router in the main router
|
| 29 |
+
app.openapi_tags = [
|
| 30 |
+
{
|
| 31 |
+
"name": r,
|
| 32 |
+
"description": main_router.get_attr(r, "description"),
|
| 33 |
+
}
|
| 34 |
+
for r in main_router.routers
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
@staticmethod
|
| 38 |
+
def add_exception_handlers(app: FastAPI):
|
| 39 |
+
"""Add exception handlers."""
|
| 40 |
+
app.exception_handlers[Exception] = ExceptionHandlers.exception
|
| 41 |
+
app.exception_handlers[ValidationError] = ExceptionHandlers.validation
|
| 42 |
+
app.exception_handlers[ResponseValidationError] = ExceptionHandlers.validation
|
| 43 |
+
app.exception_handlers[OpenBBError] = ExceptionHandlers.openbb
|
| 44 |
+
app.exception_handlers[EmptyDataError] = ExceptionHandlers.empty_data
|
| 45 |
+
app.exception_handlers[UnauthorizedError] = ExceptionHandlers.unauthorized
|
openbb_platform/core/openbb_core/api/auth/user.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User authentication."""
|
| 2 |
+
|
| 3 |
+
import secrets
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
from fastapi import Depends, HTTPException, status
|
| 7 |
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
| 8 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 9 |
+
from openbb_core.app.service.user_service import UserService
|
| 10 |
+
from openbb_core.env import Env
|
| 11 |
+
from typing_extensions import Annotated
|
| 12 |
+
|
| 13 |
+
security = HTTPBasic() if Env().API_AUTH else lambda: None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
async def authenticate_user(
|
| 17 |
+
credentials: Annotated[Optional[HTTPBasicCredentials], Depends(security)],
|
| 18 |
+
):
|
| 19 |
+
"""Authenticate the user."""
|
| 20 |
+
if credentials:
|
| 21 |
+
username = Env().API_USERNAME
|
| 22 |
+
password = Env().API_PASSWORD
|
| 23 |
+
|
| 24 |
+
is_correct_username = False
|
| 25 |
+
is_correct_password = False
|
| 26 |
+
|
| 27 |
+
if username is not None and password is not None:
|
| 28 |
+
current_username_bytes = credentials.username.encode("utf8")
|
| 29 |
+
correct_username_bytes = username.encode("utf8")
|
| 30 |
+
is_correct_username = secrets.compare_digest(
|
| 31 |
+
current_username_bytes, correct_username_bytes
|
| 32 |
+
)
|
| 33 |
+
current_password_bytes = credentials.password.encode("utf8")
|
| 34 |
+
correct_password_bytes = password.encode("utf8")
|
| 35 |
+
is_correct_password = secrets.compare_digest(
|
| 36 |
+
current_password_bytes, correct_password_bytes
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
if not (is_correct_username and is_correct_password):
|
| 40 |
+
raise HTTPException(
|
| 41 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 42 |
+
detail="Incorrect email or password",
|
| 43 |
+
headers={"WWW-Authenticate": "Basic"},
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
async def get_user_service() -> UserService:
|
| 48 |
+
"""Get user service."""
|
| 49 |
+
return UserService()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
async def get_user_settings(
|
| 53 |
+
_: Annotated[None, Depends(authenticate_user)],
|
| 54 |
+
user_service: Annotated[UserService, Depends(get_user_service)],
|
| 55 |
+
) -> UserSettings:
|
| 56 |
+
"""Get user settings."""
|
| 57 |
+
return user_service.read_from_file()
|
openbb_platform/core/openbb_core/api/dependency/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core API Dependency."""
|
openbb_platform/core/openbb_core/api/dependency/coverage.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Coverage dependency."""
|
| 2 |
+
|
| 3 |
+
from fastapi import Depends
|
| 4 |
+
from openbb_core.app.provider_interface import ProviderInterface
|
| 5 |
+
from openbb_core.app.router import CommandMap
|
| 6 |
+
from openbb_core.app.service.auth_service import AuthService
|
| 7 |
+
from typing_extensions import Annotated
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
async def get_command_map(
|
| 11 |
+
_: Annotated[None, Depends(AuthService().auth_hook)]
|
| 12 |
+
) -> CommandMap:
|
| 13 |
+
"""Get command map."""
|
| 14 |
+
return CommandMap()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
async def get_provider_interface(
|
| 18 |
+
_: Annotated[None, Depends(AuthService().auth_hook)]
|
| 19 |
+
) -> ProviderInterface:
|
| 20 |
+
"""Get provider interface."""
|
| 21 |
+
return ProviderInterface()
|
openbb_platform/core/openbb_core/api/dependency/system.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System dependency."""
|
| 2 |
+
|
| 3 |
+
from fastapi import Depends
|
| 4 |
+
from openbb_core.app.model.system_settings import SystemSettings
|
| 5 |
+
from openbb_core.app.service.auth_service import AuthService
|
| 6 |
+
from openbb_core.app.service.system_service import SystemService
|
| 7 |
+
from typing_extensions import Annotated
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
async def get_system_service() -> SystemService:
|
| 11 |
+
"""Get system service."""
|
| 12 |
+
return SystemService()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
async def get_system_settings(
|
| 16 |
+
_: Annotated[None, Depends(AuthService().auth_hook)],
|
| 17 |
+
system_service: Annotated[SystemService, Depends(get_system_service)],
|
| 18 |
+
) -> SystemSettings:
|
| 19 |
+
"""Get system settings."""
|
| 20 |
+
return system_service.system_settings
|
openbb_platform/core/openbb_core/api/exception_handlers.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exception handlers module."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from collections.abc import Iterable
|
| 7 |
+
from typing import Any, Union
|
| 8 |
+
|
| 9 |
+
from fastapi import Request
|
| 10 |
+
from fastapi.exceptions import ResponseValidationError
|
| 11 |
+
from fastapi.responses import JSONResponse, Response
|
| 12 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 13 |
+
from openbb_core.env import Env
|
| 14 |
+
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
| 15 |
+
from pydantic import ValidationError
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger("uvicorn.error")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ExceptionHandlers:
|
| 21 |
+
"""Exception handlers."""
|
| 22 |
+
|
| 23 |
+
@staticmethod
|
| 24 |
+
async def _handle(exception: Exception, status_code: int, detail: Any):
|
| 25 |
+
"""Exception handler."""
|
| 26 |
+
if Env().DEBUG_MODE:
|
| 27 |
+
raise exception
|
| 28 |
+
logger.error(exception)
|
| 29 |
+
return JSONResponse(
|
| 30 |
+
status_code=status_code,
|
| 31 |
+
content={
|
| 32 |
+
"detail": detail,
|
| 33 |
+
},
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
@staticmethod
|
| 37 |
+
async def exception(_: Request, error: Exception) -> JSONResponse:
|
| 38 |
+
"""Exception handler for Base Exception."""
|
| 39 |
+
errors = error.errors if hasattr(error, "errors") else error
|
| 40 |
+
|
| 41 |
+
if errors:
|
| 42 |
+
if isinstance(errors, ValueError):
|
| 43 |
+
return await ExceptionHandlers._handle(
|
| 44 |
+
exception=errors,
|
| 45 |
+
status_code=422,
|
| 46 |
+
detail=errors.args,
|
| 47 |
+
)
|
| 48 |
+
# Required parameters are missing and is not handled by ValidationError.
|
| 49 |
+
if isinstance(errors, Iterable):
|
| 50 |
+
for err in errors:
|
| 51 |
+
if err.get("type") == "missing":
|
| 52 |
+
return await ExceptionHandlers._handle(
|
| 53 |
+
exception=error,
|
| 54 |
+
status_code=422,
|
| 55 |
+
detail={**err},
|
| 56 |
+
)
|
| 57 |
+
return await ExceptionHandlers._handle(
|
| 58 |
+
exception=error,
|
| 59 |
+
status_code=500,
|
| 60 |
+
detail=f"Unexpected Error -> {error.__class__.__name__} -> {error}",
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
@staticmethod
|
| 64 |
+
async def validation(
|
| 65 |
+
request: Request, error: Union[ValidationError, ResponseValidationError]
|
| 66 |
+
):
|
| 67 |
+
"""Exception handler for ValidationError."""
|
| 68 |
+
# Some validation is performed at Fetcher level.
|
| 69 |
+
# So we check if the validation error comes from a QueryParams class.
|
| 70 |
+
# And that it is in the request query params.
|
| 71 |
+
# If yes, we update the error location with query.
|
| 72 |
+
# If not, we handle it as a base Exception error.
|
| 73 |
+
query_params = dict(request.query_params)
|
| 74 |
+
if isinstance(error, ResponseValidationError):
|
| 75 |
+
detail = [
|
| 76 |
+
{
|
| 77 |
+
**{k: v for k, v in err.items() if k != "ctx"},
|
| 78 |
+
"loc": ("query",) + err.get("loc", ()),
|
| 79 |
+
}
|
| 80 |
+
for err in error.errors()
|
| 81 |
+
]
|
| 82 |
+
return await ExceptionHandlers._handle(
|
| 83 |
+
exception=error,
|
| 84 |
+
status_code=422,
|
| 85 |
+
detail=detail,
|
| 86 |
+
)
|
| 87 |
+
try:
|
| 88 |
+
errors = (
|
| 89 |
+
error.errors(include_url=False)
|
| 90 |
+
if hasattr(error, "errors")
|
| 91 |
+
else error.errors
|
| 92 |
+
)
|
| 93 |
+
except Exception:
|
| 94 |
+
errors = error.errors if hasattr(error, "errors") else error
|
| 95 |
+
all_in_query = all(
|
| 96 |
+
loc in query_params for err in errors for loc in err.get("loc", ())
|
| 97 |
+
)
|
| 98 |
+
if "QueryParams" in error.title and all_in_query:
|
| 99 |
+
detail = [
|
| 100 |
+
{
|
| 101 |
+
**{k: v for k, v in err.items() if k != "ctx"},
|
| 102 |
+
"loc": ("query",) + err.get("loc", ()),
|
| 103 |
+
}
|
| 104 |
+
for err in errors
|
| 105 |
+
]
|
| 106 |
+
return await ExceptionHandlers._handle(
|
| 107 |
+
exception=error,
|
| 108 |
+
status_code=422,
|
| 109 |
+
detail=detail,
|
| 110 |
+
)
|
| 111 |
+
return await ExceptionHandlers.exception(request, error)
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
async def openbb(_: Request, error: OpenBBError):
|
| 115 |
+
"""Exception handler for OpenBBError."""
|
| 116 |
+
return await ExceptionHandlers._handle(
|
| 117 |
+
exception=error,
|
| 118 |
+
status_code=400,
|
| 119 |
+
detail=str(error.original),
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
@staticmethod
|
| 123 |
+
async def empty_data(_: Request, error: EmptyDataError):
|
| 124 |
+
"""Exception handler for EmptyDataError."""
|
| 125 |
+
return Response(status_code=204)
|
| 126 |
+
|
| 127 |
+
@staticmethod
|
| 128 |
+
async def unauthorized(_: Request, error: UnauthorizedError):
|
| 129 |
+
"""Exception handler for OpenBBError."""
|
| 130 |
+
return await ExceptionHandlers._handle(
|
| 131 |
+
exception=error,
|
| 132 |
+
status_code=502,
|
| 133 |
+
detail=str(error.original),
|
| 134 |
+
)
|
openbb_platform/core/openbb_core/api/rest_api.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""REST API for the OpenBB Platform."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from contextlib import asynccontextmanager
|
| 5 |
+
|
| 6 |
+
from fastapi import FastAPI
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from openbb_core.api.app_loader import AppLoader
|
| 9 |
+
from openbb_core.api.router.commands import router as router_commands
|
| 10 |
+
from openbb_core.api.router.coverage import router as router_coverage
|
| 11 |
+
from openbb_core.api.router.system import router as router_system
|
| 12 |
+
from openbb_core.app.service.auth_service import AuthService
|
| 13 |
+
from openbb_core.app.service.system_service import SystemService
|
| 14 |
+
from openbb_core.env import Env
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger("uvicorn.error")
|
| 17 |
+
|
| 18 |
+
system = SystemService().system_settings
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@asynccontextmanager
|
| 22 |
+
async def lifespan(_: FastAPI):
|
| 23 |
+
"""Startup event."""
|
| 24 |
+
auth = "ENABLED" if Env().API_AUTH else "DISABLED"
|
| 25 |
+
banner = rf"""
|
| 26 |
+
|
| 27 |
+
███╗
|
| 28 |
+
█████████████████╔══█████████████████╗ OpenBB Platform v{system.version}
|
| 29 |
+
███╔══════════███║ ███╔══════════███║
|
| 30 |
+
█████████████████║ █████████████████║ Authentication: {auth}
|
| 31 |
+
╚═════════════███║ ███╔═════════════╝
|
| 32 |
+
██████████████║ ██████████████╗
|
| 33 |
+
███╔═══════███║ ███╔═══════███║
|
| 34 |
+
██████████████║ ██████████████║
|
| 35 |
+
╚═════════════╝ ╚═════════════╝
|
| 36 |
+
Investment research for everyone, anywhere.
|
| 37 |
+
|
| 38 |
+
https://my.openbb.co/app/platform
|
| 39 |
+
|
| 40 |
+
"""
|
| 41 |
+
logger.info(banner)
|
| 42 |
+
yield
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
app = FastAPI(
|
| 46 |
+
title=system.api_settings.title,
|
| 47 |
+
description=system.api_settings.description,
|
| 48 |
+
version=system.api_settings.version,
|
| 49 |
+
terms_of_service=system.api_settings.terms_of_service,
|
| 50 |
+
contact={
|
| 51 |
+
"name": system.api_settings.contact_name,
|
| 52 |
+
"url": system.api_settings.contact_url,
|
| 53 |
+
"email": system.api_settings.contact_email,
|
| 54 |
+
},
|
| 55 |
+
license_info={
|
| 56 |
+
"name": system.api_settings.license_name,
|
| 57 |
+
"url": system.api_settings.license_url,
|
| 58 |
+
},
|
| 59 |
+
servers=[
|
| 60 |
+
{
|
| 61 |
+
"url": s.url,
|
| 62 |
+
"description": s.description,
|
| 63 |
+
}
|
| 64 |
+
for s in system.api_settings.servers
|
| 65 |
+
],
|
| 66 |
+
lifespan=lifespan,
|
| 67 |
+
)
|
| 68 |
+
app.add_middleware(
|
| 69 |
+
CORSMiddleware,
|
| 70 |
+
allow_origins=system.api_settings.cors.allow_origins,
|
| 71 |
+
allow_methods=system.api_settings.cors.allow_methods,
|
| 72 |
+
allow_headers=system.api_settings.cors.allow_headers,
|
| 73 |
+
)
|
| 74 |
+
AppLoader.add_routers(
|
| 75 |
+
app=app,
|
| 76 |
+
routers=(
|
| 77 |
+
[AuthService().router, router_system, router_coverage, router_commands]
|
| 78 |
+
if Env().DEV_MODE
|
| 79 |
+
else (
|
| 80 |
+
[router_commands, router_coverage]
|
| 81 |
+
if hasattr(router_commands, "routes") and router_commands.routes
|
| 82 |
+
else [router_commands]
|
| 83 |
+
)
|
| 84 |
+
),
|
| 85 |
+
prefix=system.api_settings.prefix,
|
| 86 |
+
)
|
| 87 |
+
AppLoader.add_openapi_tags(app)
|
| 88 |
+
AppLoader.add_exception_handlers(app)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
# pylint: disable=import-outside-toplevel
|
| 93 |
+
import uvicorn
|
| 94 |
+
|
| 95 |
+
# This initializes the OpenBB environment variables so they can be read before uvicorn is run.
|
| 96 |
+
Env()
|
| 97 |
+
uvicorn_kwargs = system.python_settings.model_dump().get("uvicorn", {})
|
| 98 |
+
uvicorn_reload = uvicorn_kwargs.pop("reload", None)
|
| 99 |
+
|
| 100 |
+
if uvicorn_reload is None or uvicorn_reload:
|
| 101 |
+
uvicorn_kwargs["reload"] = True
|
| 102 |
+
|
| 103 |
+
uvicorn_app = uvicorn_kwargs.pop("app", "openbb_core.api.rest_api:app")
|
| 104 |
+
|
| 105 |
+
uvicorn.run(uvicorn_app, **uvicorn_kwargs)
|
openbb_platform/core/openbb_core/api/router/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core API Router."""
|
openbb_platform/core/openbb_core/api/router/commands.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Commands: generates the command map."""
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
from functools import partial, wraps
|
| 5 |
+
from inspect import Parameter, Signature, signature
|
| 6 |
+
from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
|
| 7 |
+
|
| 8 |
+
from fastapi import APIRouter, Depends, Header
|
| 9 |
+
from fastapi.routing import APIRoute
|
| 10 |
+
from openbb_core.app.command_runner import CommandRunner
|
| 11 |
+
from openbb_core.app.model.command_context import CommandContext
|
| 12 |
+
from openbb_core.app.model.obbject import OBBject
|
| 13 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 14 |
+
from openbb_core.app.router import RouterLoader
|
| 15 |
+
from openbb_core.app.service.auth_service import AuthService
|
| 16 |
+
from openbb_core.app.service.system_service import SystemService
|
| 17 |
+
from openbb_core.app.service.user_service import UserService
|
| 18 |
+
from openbb_core.env import Env
|
| 19 |
+
from pydantic import BaseModel
|
| 20 |
+
from typing_extensions import Annotated, ParamSpec
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from openbb_charting import Charting
|
| 24 |
+
|
| 25 |
+
CHARTING_INSTALLED = True
|
| 26 |
+
except ImportError:
|
| 27 |
+
CHARTING_INSTALLED = False
|
| 28 |
+
|
| 29 |
+
T = TypeVar("T")
|
| 30 |
+
P = ParamSpec("P")
|
| 31 |
+
|
| 32 |
+
router = APIRouter(prefix="")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def build_new_annotation_map(sig: Signature) -> Dict[str, Any]:
|
| 36 |
+
"""Build new annotation map."""
|
| 37 |
+
annotation_map = {}
|
| 38 |
+
parameter_list = sig.parameters.values()
|
| 39 |
+
|
| 40 |
+
for parameter in parameter_list:
|
| 41 |
+
annotation_map[parameter.name] = parameter.annotation
|
| 42 |
+
|
| 43 |
+
annotation_map["return"] = sig.return_annotation
|
| 44 |
+
|
| 45 |
+
return annotation_map
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def build_new_signature(path: str, func: Callable) -> Signature:
|
| 49 |
+
"""Build new function signature."""
|
| 50 |
+
sig = signature(func)
|
| 51 |
+
parameter_list = sig.parameters.values()
|
| 52 |
+
return_annotation = sig.return_annotation
|
| 53 |
+
new_parameter_list = []
|
| 54 |
+
var_kw_pos = len(parameter_list)
|
| 55 |
+
for pos, parameter in enumerate(parameter_list):
|
| 56 |
+
if parameter.name == "cc" and parameter.annotation == CommandContext:
|
| 57 |
+
continue
|
| 58 |
+
|
| 59 |
+
if parameter.kind == Parameter.VAR_KEYWORD:
|
| 60 |
+
# We track VAR_KEYWORD parameter to insert the any additional
|
| 61 |
+
# parameters we need to add before it and avoid a SyntaxError
|
| 62 |
+
var_kw_pos = pos
|
| 63 |
+
|
| 64 |
+
new_parameter_list.append(
|
| 65 |
+
Parameter(
|
| 66 |
+
parameter.name,
|
| 67 |
+
kind=parameter.kind,
|
| 68 |
+
default=parameter.default,
|
| 69 |
+
annotation=parameter.annotation,
|
| 70 |
+
)
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
if CHARTING_INSTALLED and path.replace("/", "_")[1:] in Charting.functions():
|
| 74 |
+
new_parameter_list.insert(
|
| 75 |
+
var_kw_pos,
|
| 76 |
+
Parameter(
|
| 77 |
+
"chart",
|
| 78 |
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
| 79 |
+
default=False,
|
| 80 |
+
annotation=bool,
|
| 81 |
+
),
|
| 82 |
+
)
|
| 83 |
+
var_kw_pos += 1
|
| 84 |
+
|
| 85 |
+
if custom_headers := SystemService().system_settings.api_settings.custom_headers:
|
| 86 |
+
for name, default in custom_headers.items():
|
| 87 |
+
new_parameter_list.insert(
|
| 88 |
+
var_kw_pos,
|
| 89 |
+
Parameter(
|
| 90 |
+
name.replace("-", "_"),
|
| 91 |
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
| 92 |
+
default=default,
|
| 93 |
+
annotation=Annotated[
|
| 94 |
+
Optional[str], Header(include_in_schema=False)
|
| 95 |
+
],
|
| 96 |
+
),
|
| 97 |
+
)
|
| 98 |
+
var_kw_pos += 1
|
| 99 |
+
|
| 100 |
+
if Env().API_AUTH:
|
| 101 |
+
new_parameter_list.insert(
|
| 102 |
+
var_kw_pos,
|
| 103 |
+
Parameter(
|
| 104 |
+
"__authenticated_user_settings",
|
| 105 |
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
| 106 |
+
default=UserSettings(),
|
| 107 |
+
annotation=Annotated[
|
| 108 |
+
UserSettings, Depends(AuthService().user_settings_hook)
|
| 109 |
+
],
|
| 110 |
+
),
|
| 111 |
+
)
|
| 112 |
+
var_kw_pos += 1
|
| 113 |
+
|
| 114 |
+
return Signature(
|
| 115 |
+
parameters=new_parameter_list,
|
| 116 |
+
return_annotation=return_annotation,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def validate_output(c_out: OBBject) -> OBBject:
|
| 121 |
+
"""
|
| 122 |
+
Validate OBBject object.
|
| 123 |
+
|
| 124 |
+
Checks against the OBBject schema and removes fields that contain the
|
| 125 |
+
`exclude_from_api` extra `pydantic.Field` kwarg.
|
| 126 |
+
Note that the modification to the `OBBject` object is done in-place.
|
| 127 |
+
|
| 128 |
+
Parameters
|
| 129 |
+
----------
|
| 130 |
+
c_out : OBBject
|
| 131 |
+
OBBject object to validate.
|
| 132 |
+
|
| 133 |
+
Returns
|
| 134 |
+
-------
|
| 135 |
+
Dict
|
| 136 |
+
Serialized OBBject.
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
def is_model(type_):
|
| 140 |
+
return inspect.isclass(type_) and issubclass(type_, BaseModel)
|
| 141 |
+
|
| 142 |
+
def exclude_fields_from_api(key: str, value: Any):
|
| 143 |
+
type_ = type(value)
|
| 144 |
+
field = c_out.model_fields.get(key, None)
|
| 145 |
+
json_schema_extra = field.json_schema_extra if field else None
|
| 146 |
+
|
| 147 |
+
# case where 1st layer field needs to be excluded
|
| 148 |
+
if (
|
| 149 |
+
json_schema_extra
|
| 150 |
+
and isinstance(json_schema_extra, dict)
|
| 151 |
+
and json_schema_extra.get("exclude_from_api", None)
|
| 152 |
+
):
|
| 153 |
+
delattr(c_out, key)
|
| 154 |
+
|
| 155 |
+
# if it's a model with nested fields
|
| 156 |
+
elif is_model(type_):
|
| 157 |
+
for field_name, field in type_.model_fields.items():
|
| 158 |
+
extra = getattr(field, "json_schema_extra", None)
|
| 159 |
+
if (
|
| 160 |
+
extra
|
| 161 |
+
and isinstance(extra, dict)
|
| 162 |
+
and extra.get("exclude_from_api", None)
|
| 163 |
+
):
|
| 164 |
+
delattr(value, field_name)
|
| 165 |
+
|
| 166 |
+
# if it's a yet a nested model we need to go deeper in the recursion
|
| 167 |
+
elif is_model(getattr(field, "annotation", None)):
|
| 168 |
+
exclude_fields_from_api(field_name, getattr(value, field_name))
|
| 169 |
+
|
| 170 |
+
# Let a non-OBBject object pass through without validation
|
| 171 |
+
if not isinstance(c_out, OBBject):
|
| 172 |
+
return c_out
|
| 173 |
+
|
| 174 |
+
for k, v in c_out.model_copy():
|
| 175 |
+
exclude_fields_from_api(k, v)
|
| 176 |
+
|
| 177 |
+
return c_out
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def build_api_wrapper(
|
| 181 |
+
command_runner: CommandRunner,
|
| 182 |
+
route: APIRoute,
|
| 183 |
+
) -> Callable:
|
| 184 |
+
"""Build API wrapper for a command."""
|
| 185 |
+
func: Callable = route.endpoint # type: ignore
|
| 186 |
+
path: str = route.path # type: ignore
|
| 187 |
+
|
| 188 |
+
no_validate = (
|
| 189 |
+
openapi_extra.get("no_validate")
|
| 190 |
+
if (openapi_extra := getattr(route, "openapi_extra", None))
|
| 191 |
+
else None
|
| 192 |
+
)
|
| 193 |
+
new_signature = build_new_signature(path=path, func=func)
|
| 194 |
+
new_annotations_map = build_new_annotation_map(sig=new_signature)
|
| 195 |
+
func.__signature__ = new_signature # type: ignore
|
| 196 |
+
func.__annotations__ = new_annotations_map
|
| 197 |
+
|
| 198 |
+
if no_validate is True:
|
| 199 |
+
route.response_model = None
|
| 200 |
+
|
| 201 |
+
@wraps(wrapped=func)
|
| 202 |
+
async def wrapper(*args: Tuple[Any], **kwargs: Dict[str, Any]) -> OBBject:
|
| 203 |
+
user_settings: UserSettings = UserSettings.model_validate(
|
| 204 |
+
kwargs.pop(
|
| 205 |
+
"__authenticated_user_settings",
|
| 206 |
+
UserService.read_from_file(),
|
| 207 |
+
)
|
| 208 |
+
)
|
| 209 |
+
p = path.strip("/").replace("/", ".")
|
| 210 |
+
defaults = (
|
| 211 |
+
getattr(user_settings.defaults, "__dict__", {})
|
| 212 |
+
.get("commands", {})
|
| 213 |
+
.get(p, {})
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
if defaults:
|
| 217 |
+
_provider = defaults.pop("provider", None)
|
| 218 |
+
standard_params = getattr(
|
| 219 |
+
kwargs.pop("standard_params", None), "__dict__", {}
|
| 220 |
+
)
|
| 221 |
+
extra_params = getattr(kwargs.pop("extra_params", None), "__dict__", {})
|
| 222 |
+
|
| 223 |
+
if "chart" in defaults:
|
| 224 |
+
kwargs["chart"] = defaults.pop("chart", False)
|
| 225 |
+
|
| 226 |
+
if "chart_params" in defaults:
|
| 227 |
+
extra_params["chart_params"] = defaults.pop("chart_params", {})
|
| 228 |
+
|
| 229 |
+
for k, v in defaults.items():
|
| 230 |
+
if k in standard_params and standard_params[k] is None:
|
| 231 |
+
standard_params[k] = v
|
| 232 |
+
elif (k in standard_params and standard_params[k] is not None) or (
|
| 233 |
+
k in extra_params and extra_params[k] is not None
|
| 234 |
+
):
|
| 235 |
+
continue
|
| 236 |
+
elif k not in extra_params or (
|
| 237 |
+
k in extra_params and extra_params[k] is None
|
| 238 |
+
):
|
| 239 |
+
extra_params[k] = v
|
| 240 |
+
|
| 241 |
+
kwargs["standard_params"] = standard_params
|
| 242 |
+
kwargs["extra_params"] = extra_params
|
| 243 |
+
|
| 244 |
+
execute = partial(command_runner.run, path, user_settings)
|
| 245 |
+
output = await execute(*args, **kwargs)
|
| 246 |
+
|
| 247 |
+
if isinstance(output, OBBject) and not no_validate:
|
| 248 |
+
return validate_output(output)
|
| 249 |
+
|
| 250 |
+
return output
|
| 251 |
+
|
| 252 |
+
return wrapper
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def add_command_map(command_runner: CommandRunner, api_router: APIRouter) -> None:
|
| 256 |
+
"""Add command map to the API router."""
|
| 257 |
+
plugins_router = RouterLoader.from_extensions()
|
| 258 |
+
|
| 259 |
+
for route in plugins_router.api_router.routes:
|
| 260 |
+
route.endpoint = build_api_wrapper(command_runner=command_runner, route=route) # type: ignore # noqa
|
| 261 |
+
api_router.include_router(router=plugins_router.api_router)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
system_settings = SystemService(logging_sub_app="api").system_settings
|
| 265 |
+
command_runner_instance = CommandRunner(system_settings=system_settings)
|
| 266 |
+
add_command_map(command_runner=command_runner_instance, api_router=router)
|
openbb_platform/core/openbb_core/api/router/coverage.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Coverage API router."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, Depends
|
| 6 |
+
from openbb_core.api.dependency.coverage import get_command_map, get_provider_interface
|
| 7 |
+
from openbb_core.app.provider_interface import ProviderInterface
|
| 8 |
+
from openbb_core.app.router import CommandMap
|
| 9 |
+
from typing_extensions import Annotated
|
| 10 |
+
|
| 11 |
+
router = APIRouter(prefix="/coverage", tags=["Coverage"])
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@router.get("/command_model", openapi_extra={"widget_config": {"exclude": True}})
|
| 15 |
+
async def get_commands_model_map(
|
| 16 |
+
command_map: Annotated[CommandMap, Depends(get_command_map)],
|
| 17 |
+
provider_interface: Annotated[ProviderInterface, Depends(get_provider_interface)],
|
| 18 |
+
):
|
| 19 |
+
"""Get the command to provider model mapping."""
|
| 20 |
+
|
| 21 |
+
commands_map: dict = {}
|
| 22 |
+
|
| 23 |
+
for command in command_map.commands_model:
|
| 24 |
+
model = command_map.commands_model[command]
|
| 25 |
+
pi_command = provider_interface.map[model]
|
| 26 |
+
schema = provider_interface.return_annotations[model]
|
| 27 |
+
providers = list(pi_command)
|
| 28 |
+
new_command: dict = {}
|
| 29 |
+
new_command["response_schema_name"] = schema.__name__ if schema else None
|
| 30 |
+
for provider in providers:
|
| 31 |
+
new_command[provider] = {
|
| 32 |
+
"QueryParams": {"docstring": "", "fields": {}},
|
| 33 |
+
"Data": {"docstring": "", "fields": {}},
|
| 34 |
+
}
|
| 35 |
+
p = pi_command[provider]
|
| 36 |
+
query = p.get("QueryParams", {})
|
| 37 |
+
query_fields = query.get("fields", {})
|
| 38 |
+
data = p.get("Data", {})
|
| 39 |
+
data_fields = data.get("fields", {})
|
| 40 |
+
|
| 41 |
+
for field, field_info in query_fields.items():
|
| 42 |
+
attributes = (
|
| 43 |
+
field_info._attributes_set # pylint: disable=protected-access
|
| 44 |
+
)
|
| 45 |
+
if attributes.get("annotation"):
|
| 46 |
+
_annotation = str(attributes.get("annotation"))
|
| 47 |
+
attributes["annotation"] = _annotation
|
| 48 |
+
|
| 49 |
+
new_command[provider]["QueryParams"]["fields"][field] = attributes
|
| 50 |
+
|
| 51 |
+
new_command[provider]["QueryParams"]["docstring"] = query.get("docstring")
|
| 52 |
+
|
| 53 |
+
for field, field_info in data_fields.items():
|
| 54 |
+
attributes = (
|
| 55 |
+
field_info._attributes_set # pylint: disable=protected-access
|
| 56 |
+
)
|
| 57 |
+
if attributes.get("annotation"):
|
| 58 |
+
_annotation = str(attributes.get("annotation"))
|
| 59 |
+
attributes["annotation"] = _annotation
|
| 60 |
+
new_command[provider]["Data"]["fields"][field] = attributes
|
| 61 |
+
|
| 62 |
+
new_command[provider]["Data"]["docstring"] = data.get("docstring")
|
| 63 |
+
|
| 64 |
+
if openbb_info := new_command.get("openbb", {}):
|
| 65 |
+
for key in list(new_command):
|
| 66 |
+
if key == "response_schema_name":
|
| 67 |
+
continue
|
| 68 |
+
|
| 69 |
+
if obb_params := openbb_info.get("QueryParams", {}).get(
|
| 70 |
+
"fields", {}
|
| 71 |
+
):
|
| 72 |
+
old_fields = new_command[key]["QueryParams"].get("fields", {})
|
| 73 |
+
new_command[key]["QueryParams"]["fields"] = {
|
| 74 |
+
**obb_params,
|
| 75 |
+
**old_fields,
|
| 76 |
+
}
|
| 77 |
+
if obb_data := openbb_info.get("Data", {}).get("fields", {}):
|
| 78 |
+
old_fields = new_command[key]["Data"].get("fields", {})
|
| 79 |
+
new_command[key]["Data"]["fields"] = {**obb_data, **old_fields}
|
| 80 |
+
_ = new_command.pop("openbb")
|
| 81 |
+
commands_map[command] = new_command
|
| 82 |
+
|
| 83 |
+
def serializer(obj):
|
| 84 |
+
"""Serialize the object."""
|
| 85 |
+
if isinstance(obj, type):
|
| 86 |
+
return str(obj)
|
| 87 |
+
return obj
|
| 88 |
+
|
| 89 |
+
return json.loads(json.dumps(commands_map, default=serializer, indent=4))
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@router.get("/providers", openapi_extra={"widget_config": {"exclude": True}})
|
| 93 |
+
async def get_provider_coverage(
|
| 94 |
+
command_map: Annotated[CommandMap, Depends(get_command_map)]
|
| 95 |
+
):
|
| 96 |
+
"""Get command coverage by provider."""
|
| 97 |
+
return command_map.provider_coverage
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@router.get("/commands", openapi_extra={"widget_config": {"exclude": True}})
|
| 101 |
+
async def get_command_coverage(
|
| 102 |
+
command_map: Annotated[CommandMap, Depends(get_command_map)]
|
| 103 |
+
):
|
| 104 |
+
"""Get provider coverage by command."""
|
| 105 |
+
return command_map.command_coverage
|
openbb_platform/core/openbb_core/api/router/helpers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""The init of the coverage helpers."""
|
openbb_platform/core/openbb_core/api/router/helpers/coverage_helpers.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Coverage API router helper functions."""
|
| 2 |
+
|
| 3 |
+
from inspect import _empty, signature
|
| 4 |
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type
|
| 5 |
+
|
| 6 |
+
from openbb_core.app.provider_interface import ProviderInterface
|
| 7 |
+
from pydantic import BaseModel, Field, create_model
|
| 8 |
+
|
| 9 |
+
if TYPE_CHECKING:
|
| 10 |
+
from openbb_core.app.static.app_factory import BaseApp
|
| 11 |
+
|
| 12 |
+
provider_interface = ProviderInterface()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_route_callable(app: "BaseApp", route: str) -> Callable:
|
| 16 |
+
"""Get the callable for a route."""
|
| 17 |
+
# TODO: Add return typing Optional[Callable] to this function. First need to
|
| 18 |
+
# figure how to do that starting from "BaseApp" and account for the possibility
|
| 19 |
+
# of a route not existing. Then remove the type: ignore from the function.
|
| 20 |
+
|
| 21 |
+
split_route = route.replace(".", "/").split("/")[1:]
|
| 22 |
+
|
| 23 |
+
return_callable = app
|
| 24 |
+
|
| 25 |
+
for route_path in split_route:
|
| 26 |
+
return_callable = getattr(return_callable, route_path)
|
| 27 |
+
|
| 28 |
+
return return_callable # type: ignore
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def signature_to_fields(app: "BaseApp", route: str) -> Dict[str, Tuple[Any, Field]]: # type: ignore
|
| 32 |
+
"""Convert a command signature to pydantic fields."""
|
| 33 |
+
return_callable = get_route_callable(app, route)
|
| 34 |
+
sig = signature(return_callable)
|
| 35 |
+
|
| 36 |
+
fields = {}
|
| 37 |
+
for name, param in sig.parameters.items():
|
| 38 |
+
if name not in ["kwargs", "args"]:
|
| 39 |
+
type_annotation = (
|
| 40 |
+
param.annotation if param.annotation is not _empty else Any
|
| 41 |
+
)
|
| 42 |
+
description = (
|
| 43 |
+
param.annotation.__metadata__[0].description
|
| 44 |
+
if hasattr(param.annotation, "__metadata__")
|
| 45 |
+
else None
|
| 46 |
+
)
|
| 47 |
+
fields[name] = (
|
| 48 |
+
type_annotation,
|
| 49 |
+
Field(..., title="openbb", description=description),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
return fields
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def dataclass_to_fields(model_name: str) -> Dict[str, Tuple[Any, Field]]: # type: ignore
|
| 56 |
+
"""Convert a dataclass to pydantic fields."""
|
| 57 |
+
dataclass = provider_interface.params[model_name]["extra"]
|
| 58 |
+
fields = {}
|
| 59 |
+
for name, field in dataclass.__dataclass_fields__.items():
|
| 60 |
+
type_annotation = field.default.annotation if field.default is not None else Any # type: ignore
|
| 61 |
+
description = field.default.description if field.default is not None else None # type: ignore
|
| 62 |
+
title = field.default.title if field.default is not None else None # type: ignore
|
| 63 |
+
fields[name] = (
|
| 64 |
+
type_annotation,
|
| 65 |
+
Field(..., title=title, description=description),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
return fields
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def create_combined_model(
|
| 72 |
+
model_name: str,
|
| 73 |
+
*field_sets: Dict[str, Tuple[Any, Field]], # type: ignore
|
| 74 |
+
filter_by_provider: Optional[str] = None,
|
| 75 |
+
) -> Type[BaseModel]:
|
| 76 |
+
"""Create a combined pydantic model."""
|
| 77 |
+
combined_fields = {}
|
| 78 |
+
for fields in field_sets:
|
| 79 |
+
for name, (type_annotation, field) in fields.items():
|
| 80 |
+
if (
|
| 81 |
+
filter_by_provider is None
|
| 82 |
+
or "openbb" in field.title # type: ignore
|
| 83 |
+
or (filter_by_provider in field.title) # type: ignore
|
| 84 |
+
):
|
| 85 |
+
combined_fields[name] = (type_annotation, field)
|
| 86 |
+
|
| 87 |
+
model = create_model(model_name, **combined_fields) # type: ignore
|
| 88 |
+
|
| 89 |
+
# # Clean up the metadata
|
| 90 |
+
for field in model.model_fields.values():
|
| 91 |
+
if hasattr(field, "metadata"):
|
| 92 |
+
field.metadata = None # type: ignore
|
| 93 |
+
|
| 94 |
+
return model
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def get_route_schema_map(
|
| 98 |
+
app: "BaseApp",
|
| 99 |
+
command_model_map: Dict[str, str],
|
| 100 |
+
filter_by_provider: Optional[str] = None,
|
| 101 |
+
) -> Dict[str, Dict[str, Any]]:
|
| 102 |
+
"""Get the route schema map."""
|
| 103 |
+
route_schema_map = {}
|
| 104 |
+
for route, model in command_model_map.items():
|
| 105 |
+
input_model = create_combined_model(
|
| 106 |
+
route,
|
| 107 |
+
signature_to_fields(app, route),
|
| 108 |
+
dataclass_to_fields(model),
|
| 109 |
+
filter_by_provider=filter_by_provider,
|
| 110 |
+
)
|
| 111 |
+
output_model = provider_interface.return_schema[model]
|
| 112 |
+
return_callable = get_route_callable(app, route)
|
| 113 |
+
|
| 114 |
+
route_schema_map[route] = {
|
| 115 |
+
"input": input_model,
|
| 116 |
+
"output": output_model,
|
| 117 |
+
"callable": return_callable,
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return route_schema_map
|
openbb_platform/core/openbb_core/api/router/system.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System router."""
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends
|
| 4 |
+
from openbb_core.api.dependency.system import get_system_settings
|
| 5 |
+
from openbb_core.app.model.system_settings import SystemSettings
|
| 6 |
+
from typing_extensions import Annotated
|
| 7 |
+
|
| 8 |
+
router = APIRouter(prefix="/system", tags=["System"])
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@router.get("")
|
| 12 |
+
async def get_system_model(
|
| 13 |
+
system_settings: Annotated[SystemSettings, Depends(get_system_settings)],
|
| 14 |
+
):
|
| 15 |
+
"""Get system model."""
|
| 16 |
+
return system_settings
|
openbb_platform/core/openbb_core/api/router/user.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Platform API Account Router."""
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends
|
| 4 |
+
from openbb_core.api.auth.user import authenticate_user, get_user_settings
|
| 5 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 6 |
+
from typing_extensions import Annotated
|
| 7 |
+
|
| 8 |
+
router = APIRouter(prefix="/user", tags=["User"])
|
| 9 |
+
auth_hook = authenticate_user
|
| 10 |
+
user_settings_hook = get_user_settings
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@router.get("/me")
|
| 14 |
+
async def read_user_settings(
|
| 15 |
+
user_settings: Annotated[UserSettings, Depends(get_user_settings)]
|
| 16 |
+
):
|
| 17 |
+
"""Read current user settings."""
|
| 18 |
+
return user_settings
|
openbb_platform/core/openbb_core/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core App Module."""
|
openbb_platform/core/openbb_core/app/command_runner.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command runner module."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=R0903
|
| 4 |
+
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from dataclasses import asdict, is_dataclass
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from inspect import Parameter, signature
|
| 9 |
+
from sys import exc_info
|
| 10 |
+
from time import perf_counter_ns
|
| 11 |
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Type
|
| 12 |
+
from warnings import catch_warnings, showwarning, warn
|
| 13 |
+
|
| 14 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 15 |
+
from openbb_core.app.model.abstract.warning import OpenBBWarning, cast_warning
|
| 16 |
+
from openbb_core.app.model.metadata import Metadata
|
| 17 |
+
from openbb_core.app.model.obbject import OBBject
|
| 18 |
+
from openbb_core.app.provider_interface import ExtraParams
|
| 19 |
+
from openbb_core.app.static.package_builder import PathHandler
|
| 20 |
+
from openbb_core.env import Env
|
| 21 |
+
from openbb_core.provider.utils.helpers import maybe_coroutine, run_async
|
| 22 |
+
from pydantic import BaseModel, ConfigDict, create_model
|
| 23 |
+
|
| 24 |
+
if TYPE_CHECKING:
|
| 25 |
+
from fastapi.routing import APIRoute
|
| 26 |
+
from openbb_core.app.model.system_settings import SystemSettings
|
| 27 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 28 |
+
from openbb_core.app.router import CommandMap
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ExecutionContext:
|
| 32 |
+
"""Execution context."""
|
| 33 |
+
|
| 34 |
+
# For checking if the command specifies no validation in the API Route
|
| 35 |
+
_route_map = PathHandler.build_route_map()
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
command_map: "CommandMap",
|
| 40 |
+
route: str,
|
| 41 |
+
system_settings: "SystemSettings",
|
| 42 |
+
user_settings: "UserSettings",
|
| 43 |
+
) -> None:
|
| 44 |
+
"""Initialize the execution context."""
|
| 45 |
+
self.command_map = command_map
|
| 46 |
+
self.route = route
|
| 47 |
+
self.system_settings = system_settings
|
| 48 |
+
self.user_settings = user_settings
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def api_route(self) -> "APIRoute":
|
| 52 |
+
"""API route."""
|
| 53 |
+
return self._route_map[self.route]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class ParametersBuilder:
|
| 57 |
+
"""Build parameters for a function."""
|
| 58 |
+
|
| 59 |
+
@staticmethod
|
| 60 |
+
def get_polished_parameter_list(func: Callable) -> List[Parameter]:
|
| 61 |
+
"""Get the signature parameters values as a list."""
|
| 62 |
+
sig = signature(func)
|
| 63 |
+
parameter_list = list(sig.parameters.values())
|
| 64 |
+
|
| 65 |
+
return parameter_list
|
| 66 |
+
|
| 67 |
+
@staticmethod
|
| 68 |
+
def get_polished_func(func: Callable) -> Callable:
|
| 69 |
+
"""Remove __authenticated_user_settings from the function signature and annotations."""
|
| 70 |
+
func = deepcopy(func)
|
| 71 |
+
sig = signature(func)
|
| 72 |
+
parameter_map = dict(sig.parameters)
|
| 73 |
+
|
| 74 |
+
if "__authenticated_user_settings" in parameter_map:
|
| 75 |
+
parameter_map.pop("__authenticated_user_settings")
|
| 76 |
+
|
| 77 |
+
parameter_list = list(parameter_map.values())
|
| 78 |
+
new_signature = signature(func).replace(parameters=parameter_list)
|
| 79 |
+
|
| 80 |
+
func.__signature__ = new_signature # type: ignore
|
| 81 |
+
func.__annotations__ = parameter_map
|
| 82 |
+
|
| 83 |
+
return func
|
| 84 |
+
|
| 85 |
+
@classmethod
|
| 86 |
+
def merge_args_and_kwargs(
|
| 87 |
+
cls,
|
| 88 |
+
func: Callable,
|
| 89 |
+
args: Tuple[Any, ...],
|
| 90 |
+
kwargs: Dict[str, Any],
|
| 91 |
+
) -> Dict[str, Any]:
|
| 92 |
+
"""Merge args and kwargs into a single dict."""
|
| 93 |
+
args = deepcopy(args)
|
| 94 |
+
kwargs = deepcopy(kwargs)
|
| 95 |
+
parameter_list = cls.get_polished_parameter_list(func=func)
|
| 96 |
+
parameter_map = {}
|
| 97 |
+
|
| 98 |
+
for index, parameter in enumerate(parameter_list):
|
| 99 |
+
if index < len(args):
|
| 100 |
+
parameter_map[parameter.name] = args[index]
|
| 101 |
+
elif parameter.name in kwargs:
|
| 102 |
+
parameter_map[parameter.name] = kwargs[parameter.name]
|
| 103 |
+
elif parameter.default is not parameter.empty:
|
| 104 |
+
parameter_map[parameter.name] = parameter.default
|
| 105 |
+
else:
|
| 106 |
+
parameter_map[parameter.name] = None
|
| 107 |
+
|
| 108 |
+
return parameter_map
|
| 109 |
+
|
| 110 |
+
@staticmethod
|
| 111 |
+
def update_command_context(
|
| 112 |
+
func: Callable,
|
| 113 |
+
kwargs: Dict[str, Any],
|
| 114 |
+
system_settings: "SystemSettings",
|
| 115 |
+
user_settings: "UserSettings",
|
| 116 |
+
) -> Dict[str, Any]:
|
| 117 |
+
"""Update the command context with the available user and system settings."""
|
| 118 |
+
# pylint: disable=import-outside-toplevel
|
| 119 |
+
from openbb_core.app.model.command_context import CommandContext
|
| 120 |
+
|
| 121 |
+
argcount = func.__code__.co_argcount
|
| 122 |
+
if "cc" in func.__code__.co_varnames[:argcount]:
|
| 123 |
+
kwargs["cc"] = CommandContext(
|
| 124 |
+
user_settings=user_settings,
|
| 125 |
+
system_settings=system_settings,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
return kwargs
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def _warn_kwargs(
|
| 132 |
+
extra_params: Dict[str, Any],
|
| 133 |
+
model: Type[BaseModel],
|
| 134 |
+
) -> None:
|
| 135 |
+
"""Warn if kwargs received and ignored by the validation model."""
|
| 136 |
+
# We only check the extra_params annotation because ignored fields
|
| 137 |
+
# will always be there
|
| 138 |
+
annotation = getattr(
|
| 139 |
+
model.model_fields.get("extra_params", None), "annotation", None
|
| 140 |
+
)
|
| 141 |
+
if is_dataclass(annotation) and any(
|
| 142 |
+
t is ExtraParams for t in getattr(annotation, "__bases__", [])
|
| 143 |
+
):
|
| 144 |
+
valid = asdict(annotation()) # type: ignore
|
| 145 |
+
for p in extra_params:
|
| 146 |
+
if "chart_params" in p:
|
| 147 |
+
continue
|
| 148 |
+
if p not in valid:
|
| 149 |
+
warn(
|
| 150 |
+
message=f"Parameter '{p}' not found.",
|
| 151 |
+
category=OpenBBWarning,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
@staticmethod
|
| 155 |
+
def _as_dict(obj: Any) -> Dict[str, Any]:
|
| 156 |
+
"""Safely convert an object to a dict."""
|
| 157 |
+
try:
|
| 158 |
+
if isinstance(obj, dict):
|
| 159 |
+
return obj
|
| 160 |
+
return asdict(obj) if is_dataclass(obj) else dict(obj) # type: ignore
|
| 161 |
+
except Exception:
|
| 162 |
+
return {}
|
| 163 |
+
|
| 164 |
+
@staticmethod
|
| 165 |
+
def validate_kwargs(
|
| 166 |
+
func: Callable,
|
| 167 |
+
kwargs: Dict[str, Any],
|
| 168 |
+
) -> Dict[str, Any]:
|
| 169 |
+
"""Validate kwargs and if possible coerce to the correct type."""
|
| 170 |
+
sig = signature(func)
|
| 171 |
+
fields = {
|
| 172 |
+
n: (
|
| 173 |
+
Any if p.annotation is Parameter.empty else p.annotation,
|
| 174 |
+
... if p.default is Parameter.empty else p.default,
|
| 175 |
+
)
|
| 176 |
+
for n, p in sig.parameters.items()
|
| 177 |
+
}
|
| 178 |
+
# We allow extra fields to return with model with 'cc: CommandContext'
|
| 179 |
+
config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
| 180 |
+
# pylint: disable=C0103
|
| 181 |
+
ValidationModel = create_model(func.__name__, __config__=config, **fields) # type: ignore
|
| 182 |
+
# Validate and coerce
|
| 183 |
+
model = ValidationModel(**kwargs)
|
| 184 |
+
ParametersBuilder._warn_kwargs(
|
| 185 |
+
ParametersBuilder._as_dict(kwargs.get("extra_params", {})),
|
| 186 |
+
ValidationModel,
|
| 187 |
+
)
|
| 188 |
+
return dict(model)
|
| 189 |
+
|
| 190 |
+
# pylint: disable=R0913
|
| 191 |
+
@classmethod
|
| 192 |
+
def build(
|
| 193 |
+
cls,
|
| 194 |
+
args: Tuple[Any, ...],
|
| 195 |
+
execution_context: ExecutionContext,
|
| 196 |
+
func: Callable,
|
| 197 |
+
kwargs: Dict[str, Any],
|
| 198 |
+
) -> Dict[str, Any]:
|
| 199 |
+
"""Build the parameters for a function."""
|
| 200 |
+
func = cls.get_polished_func(func=func)
|
| 201 |
+
system_settings = execution_context.system_settings
|
| 202 |
+
user_settings = execution_context.user_settings
|
| 203 |
+
|
| 204 |
+
kwargs = cls.merge_args_and_kwargs(
|
| 205 |
+
func=func,
|
| 206 |
+
args=args,
|
| 207 |
+
kwargs=kwargs,
|
| 208 |
+
)
|
| 209 |
+
kwargs = cls.update_command_context(
|
| 210 |
+
func=func,
|
| 211 |
+
kwargs=kwargs,
|
| 212 |
+
system_settings=system_settings,
|
| 213 |
+
user_settings=user_settings,
|
| 214 |
+
)
|
| 215 |
+
kwargs = cls.validate_kwargs(
|
| 216 |
+
func=func,
|
| 217 |
+
kwargs=kwargs,
|
| 218 |
+
)
|
| 219 |
+
return kwargs
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
# pylint: disable=too-few-public-methods
|
| 223 |
+
class StaticCommandRunner:
|
| 224 |
+
"""Static Command Runner."""
|
| 225 |
+
|
| 226 |
+
@classmethod
|
| 227 |
+
async def _command(
|
| 228 |
+
cls,
|
| 229 |
+
func: Callable,
|
| 230 |
+
kwargs: Dict[str, Any],
|
| 231 |
+
show_warnings: bool = True, # pylint: disable=unused-argument # type: ignore
|
| 232 |
+
) -> OBBject:
|
| 233 |
+
"""Run a command and return the output."""
|
| 234 |
+
obbject = await maybe_coroutine(func, **kwargs)
|
| 235 |
+
if isinstance(obbject, OBBject):
|
| 236 |
+
obbject.provider = getattr(
|
| 237 |
+
kwargs.get("provider_choices"),
|
| 238 |
+
"provider",
|
| 239 |
+
getattr(obbject, "provider", None),
|
| 240 |
+
)
|
| 241 |
+
return obbject
|
| 242 |
+
|
| 243 |
+
@classmethod
|
| 244 |
+
def _chart(
|
| 245 |
+
cls,
|
| 246 |
+
obbject: OBBject,
|
| 247 |
+
**kwargs,
|
| 248 |
+
) -> None:
|
| 249 |
+
"""Create a chart from the command output."""
|
| 250 |
+
try:
|
| 251 |
+
if "charting" not in obbject.accessors:
|
| 252 |
+
raise OpenBBError(
|
| 253 |
+
"Charting is not installed. Please install `openbb-charting`."
|
| 254 |
+
)
|
| 255 |
+
# Here we will pop the chart_params kwargs and flatten them into the kwargs.
|
| 256 |
+
chart_params = {}
|
| 257 |
+
extra_params = getattr(obbject, "_extra_params", {})
|
| 258 |
+
|
| 259 |
+
if extra_params and "chart_params" in extra_params:
|
| 260 |
+
chart_params = extra_params.get("chart_params", {})
|
| 261 |
+
|
| 262 |
+
if kwargs.get("chart_params"):
|
| 263 |
+
chart_params.update(kwargs.pop("chart_params", {}))
|
| 264 |
+
# Verify that kwargs is not nested as kwargs so we don't miss any chart params.
|
| 265 |
+
if (
|
| 266 |
+
"kwargs" in kwargs
|
| 267 |
+
and "chart_params" in kwargs["kwargs"]
|
| 268 |
+
and kwargs["kwargs"].get("chart_params")
|
| 269 |
+
):
|
| 270 |
+
chart_params.update(kwargs.pop("kwargs", {}).get("chart_params", {}))
|
| 271 |
+
|
| 272 |
+
if chart_params:
|
| 273 |
+
kwargs.update(chart_params)
|
| 274 |
+
obbject.charting.show(render=False, **kwargs) # type: ignore[attr-defined]
|
| 275 |
+
except Exception as e: # pylint: disable=broad-exception-caught
|
| 276 |
+
if Env().DEBUG_MODE:
|
| 277 |
+
raise OpenBBError(e) from e
|
| 278 |
+
warn(str(e), OpenBBWarning)
|
| 279 |
+
|
| 280 |
+
@classmethod
|
| 281 |
+
def _extract_params(cls, kwargs, key) -> Dict:
|
| 282 |
+
"""Extract params models from kwargs and convert to a dictionary."""
|
| 283 |
+
params = kwargs.get(key, {})
|
| 284 |
+
if hasattr(params, "__dict__"):
|
| 285 |
+
return params.__dict__
|
| 286 |
+
return params
|
| 287 |
+
|
| 288 |
+
# pylint: disable=R0913, R0914
|
| 289 |
+
@classmethod
|
| 290 |
+
async def _execute_func( # pylint: disable=too-many-positional-arguments
|
| 291 |
+
cls,
|
| 292 |
+
route: str,
|
| 293 |
+
args: Tuple[Any, ...],
|
| 294 |
+
execution_context: ExecutionContext,
|
| 295 |
+
func: Callable,
|
| 296 |
+
kwargs: Dict[str, Any],
|
| 297 |
+
) -> OBBject:
|
| 298 |
+
"""Execute a function and return the output."""
|
| 299 |
+
# pylint: disable=import-outside-toplevel
|
| 300 |
+
from openbb_core.app.logs.logging_service import LoggingService
|
| 301 |
+
|
| 302 |
+
user_settings = execution_context.user_settings
|
| 303 |
+
system_settings = execution_context.system_settings
|
| 304 |
+
raised_warnings: list = []
|
| 305 |
+
custom_headers: Optional[dict[str, Any]] = None
|
| 306 |
+
|
| 307 |
+
try:
|
| 308 |
+
with catch_warnings(record=True) as warning_list:
|
| 309 |
+
# If we're on Jupyter we need to pop here because we will lose "chart" after
|
| 310 |
+
# ParametersBuilder.build. This needs to be fixed in a way that chart is
|
| 311 |
+
# added to the function signature and shared for jupyter and api
|
| 312 |
+
# We can check in the router decorator if the given function has a chart
|
| 313 |
+
# in the charting extension then we add it there. This way we can remove
|
| 314 |
+
# the chart parameter from the commands.py and package_builder, it will be
|
| 315 |
+
# added to the function signature in the router decorator
|
| 316 |
+
chart = kwargs.pop("chart", False)
|
| 317 |
+
|
| 318 |
+
kwargs = ParametersBuilder.build(
|
| 319 |
+
args=args,
|
| 320 |
+
execution_context=execution_context,
|
| 321 |
+
func=func,
|
| 322 |
+
kwargs=kwargs,
|
| 323 |
+
)
|
| 324 |
+
kwargs = kwargs if kwargs is not None else {}
|
| 325 |
+
# If we're on the api we need to remove "chart" here because the parameter is added on
|
| 326 |
+
# commands.py and the function signature does not expect "chart"
|
| 327 |
+
kwargs.pop("chart", None)
|
| 328 |
+
# We also pop custom headers
|
| 329 |
+
model_headers = system_settings.api_settings.custom_headers or {}
|
| 330 |
+
custom_headers = {
|
| 331 |
+
name: kwargs.pop(name.replace("-", "_"), default)
|
| 332 |
+
for name, default in model_headers.items() or {}
|
| 333 |
+
} or None
|
| 334 |
+
|
| 335 |
+
obbject = await cls._command(func, kwargs)
|
| 336 |
+
# The output might be from a router command with 'no_validate=True'
|
| 337 |
+
# It might be of a different type than OBBject.
|
| 338 |
+
# In this case, we avoid accessing those attributes.
|
| 339 |
+
if isinstance(obbject, OBBject):
|
| 340 |
+
# This section prepares the obbject to pass to the charting service.
|
| 341 |
+
obbject._route = route # pylint: disable=protected-access
|
| 342 |
+
std_params = cls._extract_params(kwargs, "standard_params") or (
|
| 343 |
+
kwargs if "data" in kwargs else {}
|
| 344 |
+
)
|
| 345 |
+
extra_params = cls._extract_params(kwargs, "extra_params")
|
| 346 |
+
obbject._standard_params = ( # pylint: disable=protected-access
|
| 347 |
+
std_params
|
| 348 |
+
)
|
| 349 |
+
obbject._extra_params = ( # pylint: disable=protected-access
|
| 350 |
+
extra_params
|
| 351 |
+
)
|
| 352 |
+
if chart and obbject.results:
|
| 353 |
+
cls._chart(obbject, **kwargs)
|
| 354 |
+
|
| 355 |
+
raised_warnings = warning_list if warning_list else []
|
| 356 |
+
finally:
|
| 357 |
+
if raised_warnings:
|
| 358 |
+
if isinstance(obbject, OBBject):
|
| 359 |
+
obbject.warnings = []
|
| 360 |
+
for w in raised_warnings:
|
| 361 |
+
if isinstance(obbject, OBBject):
|
| 362 |
+
obbject.warnings.append(cast_warning(w))
|
| 363 |
+
if user_settings.preferences.show_warnings:
|
| 364 |
+
showwarning(
|
| 365 |
+
message=w.message,
|
| 366 |
+
category=w.category,
|
| 367 |
+
filename=w.filename,
|
| 368 |
+
lineno=w.lineno,
|
| 369 |
+
file=w.file,
|
| 370 |
+
line=w.line,
|
| 371 |
+
)
|
| 372 |
+
ls = LoggingService(system_settings, user_settings)
|
| 373 |
+
ls.log(
|
| 374 |
+
user_settings=user_settings,
|
| 375 |
+
system_settings=system_settings,
|
| 376 |
+
route=route,
|
| 377 |
+
func=func,
|
| 378 |
+
kwargs=kwargs,
|
| 379 |
+
exec_info=exc_info(),
|
| 380 |
+
custom_headers=custom_headers,
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
return obbject
|
| 384 |
+
|
| 385 |
+
# pylint: disable=W0718
|
| 386 |
+
@classmethod
|
| 387 |
+
async def run(
|
| 388 |
+
cls,
|
| 389 |
+
execution_context: ExecutionContext,
|
| 390 |
+
/,
|
| 391 |
+
*args,
|
| 392 |
+
**kwargs,
|
| 393 |
+
) -> OBBject:
|
| 394 |
+
"""Run a command and return the OBBject as output."""
|
| 395 |
+
timestamp = datetime.now()
|
| 396 |
+
start_ns = perf_counter_ns()
|
| 397 |
+
|
| 398 |
+
command_map = execution_context.command_map
|
| 399 |
+
route = execution_context.route
|
| 400 |
+
|
| 401 |
+
if func := command_map.get_command(route=route):
|
| 402 |
+
obbject = await cls._execute_func(
|
| 403 |
+
route=route,
|
| 404 |
+
args=args, # type: ignore
|
| 405 |
+
execution_context=execution_context,
|
| 406 |
+
func=func,
|
| 407 |
+
kwargs=kwargs,
|
| 408 |
+
)
|
| 409 |
+
else:
|
| 410 |
+
raise AttributeError(f"Invalid command : route={route}")
|
| 411 |
+
|
| 412 |
+
duration = perf_counter_ns() - start_ns
|
| 413 |
+
|
| 414 |
+
if execution_context.user_settings.preferences.metadata and isinstance(
|
| 415 |
+
obbject, OBBject
|
| 416 |
+
):
|
| 417 |
+
try:
|
| 418 |
+
obbject.extra["metadata"] = Metadata(
|
| 419 |
+
arguments=kwargs,
|
| 420 |
+
duration=duration,
|
| 421 |
+
route=route,
|
| 422 |
+
timestamp=timestamp,
|
| 423 |
+
)
|
| 424 |
+
except Exception as e:
|
| 425 |
+
if Env().DEBUG_MODE:
|
| 426 |
+
raise OpenBBError(e) from e
|
| 427 |
+
warn(str(e), OpenBBWarning)
|
| 428 |
+
|
| 429 |
+
return obbject
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
class CommandRunner:
|
| 433 |
+
"""Command runner."""
|
| 434 |
+
|
| 435 |
+
def __init__(
|
| 436 |
+
self,
|
| 437 |
+
command_map: Optional["CommandMap"] = None,
|
| 438 |
+
system_settings: Optional["SystemSettings"] = None,
|
| 439 |
+
user_settings: Optional["UserSettings"] = None,
|
| 440 |
+
) -> None:
|
| 441 |
+
"""Initialize the command runner."""
|
| 442 |
+
# pylint: disable=import-outside-toplevel
|
| 443 |
+
from openbb_core.app.router import CommandMap
|
| 444 |
+
from openbb_core.app.service.system_service import SystemService
|
| 445 |
+
from openbb_core.app.service.user_service import UserService
|
| 446 |
+
|
| 447 |
+
self._command_map = command_map or CommandMap()
|
| 448 |
+
self._system_settings = system_settings or SystemService().system_settings
|
| 449 |
+
self._user_settings = user_settings or UserService.read_from_file()
|
| 450 |
+
|
| 451 |
+
def init_logging_service(self) -> None:
|
| 452 |
+
"""Initialize the logging service."""
|
| 453 |
+
# pylint: disable=import-outside-toplevel
|
| 454 |
+
from openbb_core.app.logs.logging_service import LoggingService
|
| 455 |
+
|
| 456 |
+
_ = LoggingService(
|
| 457 |
+
system_settings=self._system_settings, user_settings=self._user_settings
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
@property
|
| 461 |
+
def command_map(self) -> "CommandMap":
|
| 462 |
+
"""Command map."""
|
| 463 |
+
return self._command_map
|
| 464 |
+
|
| 465 |
+
@property
|
| 466 |
+
def system_settings(self) -> "SystemSettings":
|
| 467 |
+
"""System settings."""
|
| 468 |
+
return self._system_settings
|
| 469 |
+
|
| 470 |
+
@property
|
| 471 |
+
def user_settings(self) -> "UserSettings":
|
| 472 |
+
"""User settings."""
|
| 473 |
+
return self._user_settings
|
| 474 |
+
|
| 475 |
+
@user_settings.setter
|
| 476 |
+
def user_settings(self, user_settings: "UserSettings") -> None:
|
| 477 |
+
self._user_settings = user_settings
|
| 478 |
+
|
| 479 |
+
# pylint: disable=W1113
|
| 480 |
+
async def run(
|
| 481 |
+
self,
|
| 482 |
+
route: str,
|
| 483 |
+
user_settings: Optional["UserSettings"] = None,
|
| 484 |
+
/,
|
| 485 |
+
*args,
|
| 486 |
+
**kwargs,
|
| 487 |
+
) -> OBBject:
|
| 488 |
+
"""Run a command and return the OBBject as output."""
|
| 489 |
+
# pylint: disable=import-outside-toplevel
|
| 490 |
+
|
| 491 |
+
self._user_settings = user_settings or self._user_settings
|
| 492 |
+
|
| 493 |
+
execution_context = ExecutionContext(
|
| 494 |
+
command_map=self._command_map,
|
| 495 |
+
route=route,
|
| 496 |
+
system_settings=self._system_settings,
|
| 497 |
+
user_settings=self._user_settings,
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
return await StaticCommandRunner.run(execution_context, *args, **kwargs)
|
| 501 |
+
|
| 502 |
+
# pylint: disable=W1113
|
| 503 |
+
def sync_run(
|
| 504 |
+
self,
|
| 505 |
+
route: str,
|
| 506 |
+
user_settings: Optional["UserSettings"] = None,
|
| 507 |
+
/,
|
| 508 |
+
*args,
|
| 509 |
+
**kwargs,
|
| 510 |
+
) -> OBBject:
|
| 511 |
+
"""Run a command and return the OBBject as output."""
|
| 512 |
+
return run_async(self.run, route, user_settings, *args, **kwargs)
|
openbb_platform/core/openbb_core/app/constants.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Constants for the OpenBB Platform."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
HOME_DIRECTORY = Path.home()
|
| 6 |
+
OPENBB_DIRECTORY = Path(HOME_DIRECTORY, ".openbb_platform")
|
| 7 |
+
USER_SETTINGS_PATH = Path(OPENBB_DIRECTORY, "user_settings.json")
|
| 8 |
+
SYSTEM_SETTINGS_PATH = Path(OPENBB_DIRECTORY, "system_settings.json")
|
openbb_platform/core/openbb_core/app/deprecation.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenBB-specific deprecation warnings.
|
| 3 |
+
|
| 4 |
+
This implementation was inspired from Pydantic's specific warnings and modified to suit OpenBB's needs.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from typing import Optional, Tuple
|
| 8 |
+
|
| 9 |
+
from openbb_core.app.version import VERSION, get_major_minor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class DeprecationSummary(str):
|
| 13 |
+
"""A string subclass that can be used to store deprecation metadata."""
|
| 14 |
+
|
| 15 |
+
def __new__(cls, value: str, metadata: DeprecationWarning):
|
| 16 |
+
"""Create a new instance of the class."""
|
| 17 |
+
obj = str.__new__(cls, value)
|
| 18 |
+
setattr(obj, "metadata", metadata)
|
| 19 |
+
return obj
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class OpenBBDeprecationWarning(DeprecationWarning):
|
| 23 |
+
"""
|
| 24 |
+
A OpenBB specific deprecation warning.
|
| 25 |
+
|
| 26 |
+
This warning is raised when using deprecated functionality in OpenBB. It provides information on when the
|
| 27 |
+
deprecation was introduced and the expected version in which the corresponding functionality will be removed.
|
| 28 |
+
|
| 29 |
+
Attributes
|
| 30 |
+
----------
|
| 31 |
+
message: Description of the warning.
|
| 32 |
+
since: Version in what the deprecation was introduced.
|
| 33 |
+
expected_removal: Version in what the corresponding functionality expected to be removed.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
# The choice to use class variables is based on the potential for extending the class in future developments.
|
| 37 |
+
# Example: launching Platform V5 and decide to create a subclimagine we areass named OpenBBDeprecatedSinceV4,
|
| 38 |
+
# which inherits from OpenBBDeprecationWarning. In this subclass, we would set since=4.X and expected_removal=5.0.
|
| 39 |
+
# It's important for these values to be defined at the class level, rather than just at the instance level,
|
| 40 |
+
# to ensure consistency and clarity in our deprecation warnings across the platform.
|
| 41 |
+
|
| 42 |
+
message: str
|
| 43 |
+
since: Tuple[int, int]
|
| 44 |
+
expected_removal: Tuple[int, int]
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
message: str,
|
| 49 |
+
*args: object,
|
| 50 |
+
since: Optional[Tuple[int, int]] = None,
|
| 51 |
+
expected_removal: Optional[Tuple[int, int]] = None,
|
| 52 |
+
) -> None:
|
| 53 |
+
"""Initialize the warning."""
|
| 54 |
+
super().__init__(message, *args)
|
| 55 |
+
self.message = message.rstrip(".")
|
| 56 |
+
self.since = since or get_major_minor(VERSION)
|
| 57 |
+
self.expected_removal = expected_removal or (self.since[0] + 1, 0)
|
| 58 |
+
self.long_message = (
|
| 59 |
+
f"{self.message}. Deprecated in OpenBB Platform V{self.since[0]}.{self.since[1]}"
|
| 60 |
+
f" to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
def __str__(self) -> str:
|
| 64 |
+
"""Return the warning message."""
|
| 65 |
+
return self.long_message
|
openbb_platform/core/openbb_core/app/extension_loader.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extension Loader."""
|
| 2 |
+
|
| 3 |
+
from enum import Enum
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
from importlib_metadata import EntryPoint, EntryPoints, entry_points
|
| 8 |
+
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
| 9 |
+
from openbb_core.app.model.extension import Extension
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
from openbb_core.app.router import Router
|
| 13 |
+
from openbb_core.provider.abstract.provider import Provider
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class OpenBBGroups(Enum):
|
| 17 |
+
"""OpenBB Extension Groups."""
|
| 18 |
+
|
| 19 |
+
core = "openbb_core_extension"
|
| 20 |
+
provider = "openbb_provider_extension"
|
| 21 |
+
obbject = "openbb_obbject_extension"
|
| 22 |
+
|
| 23 |
+
@staticmethod
|
| 24 |
+
def groups() -> List[str]:
|
| 25 |
+
"""Return the OpenBBGroups."""
|
| 26 |
+
return [
|
| 27 |
+
OpenBBGroups.core.value,
|
| 28 |
+
OpenBBGroups.provider.value,
|
| 29 |
+
OpenBBGroups.obbject.value,
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ExtensionLoader(metaclass=SingletonMeta):
|
| 34 |
+
"""Extension loader class."""
|
| 35 |
+
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
) -> None:
|
| 39 |
+
"""Initialize the extension loader."""
|
| 40 |
+
self._obbject_entry_points: EntryPoints = self._sorted_entry_points(
|
| 41 |
+
group=OpenBBGroups.obbject.value
|
| 42 |
+
)
|
| 43 |
+
self._core_entry_points: EntryPoints = self._sorted_entry_points(
|
| 44 |
+
group=OpenBBGroups.core.value
|
| 45 |
+
)
|
| 46 |
+
self._provider_entry_points: EntryPoints = self._sorted_entry_points(
|
| 47 |
+
group=OpenBBGroups.provider.value
|
| 48 |
+
)
|
| 49 |
+
self._obbject_objects: Dict[str, Extension] = {}
|
| 50 |
+
self._core_objects: Dict[str, Router] = {}
|
| 51 |
+
self._provider_objects: Dict[str, Provider] = {}
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def obbject_entry_points(self) -> EntryPoints:
|
| 55 |
+
"""Return the obbject entry points."""
|
| 56 |
+
return self._obbject_entry_points
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def core_entry_points(self) -> EntryPoints:
|
| 60 |
+
"""Return the core entry points."""
|
| 61 |
+
return self._core_entry_points
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def provider_entry_points(self) -> EntryPoints:
|
| 65 |
+
"""Return the provider entry points."""
|
| 66 |
+
return self._provider_entry_points
|
| 67 |
+
|
| 68 |
+
@property
|
| 69 |
+
def entry_points(self) -> List[EntryPoints]:
|
| 70 |
+
"""Return the entry points."""
|
| 71 |
+
return [
|
| 72 |
+
self._core_entry_points,
|
| 73 |
+
self._provider_entry_points,
|
| 74 |
+
self._obbject_entry_points,
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
@staticmethod
|
| 78 |
+
def _get_entry_point(
|
| 79 |
+
entry_points_: EntryPoints, ext_name: str
|
| 80 |
+
) -> Optional[EntryPoint]:
|
| 81 |
+
"""Given an extension name and a list of entry points, return the corresponding entry point."""
|
| 82 |
+
return next((ep for ep in entry_points_ if ep.name == ext_name), None)
|
| 83 |
+
|
| 84 |
+
def get_obbject_entry_point(self, ext_name: str) -> Optional[EntryPoint]:
|
| 85 |
+
"""Given an extension name, return the corresponding entry point."""
|
| 86 |
+
return self._get_entry_point(self._obbject_entry_points, ext_name)
|
| 87 |
+
|
| 88 |
+
def get_core_entry_point(self, ext_name: str) -> Optional[EntryPoint]:
|
| 89 |
+
"""Given an extension name, return the corresponding entry point."""
|
| 90 |
+
return self._get_entry_point(self._core_entry_points, ext_name)
|
| 91 |
+
|
| 92 |
+
def get_provider_entry_point(self, ext_name: str) -> Optional[EntryPoint]:
|
| 93 |
+
"""Given an extension name, return the corresponding entry point."""
|
| 94 |
+
return self._get_entry_point(self._provider_entry_points, ext_name)
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
@lru_cache
|
| 98 |
+
def obbject_objects(self) -> Dict[str, Extension]:
|
| 99 |
+
"""Return a dict of obbject extension objects."""
|
| 100 |
+
self._obbject_objects = self._load_entry_points(
|
| 101 |
+
self._obbject_entry_points, OpenBBGroups.obbject
|
| 102 |
+
)
|
| 103 |
+
return self._obbject_objects
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
@lru_cache
|
| 107 |
+
def core_objects(self) -> Dict[str, "Router"]:
|
| 108 |
+
"""Return a dict of core extension objects."""
|
| 109 |
+
self._core_objects = self._load_entry_points(
|
| 110 |
+
self._core_entry_points, OpenBBGroups.core
|
| 111 |
+
)
|
| 112 |
+
return self._core_objects
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
@lru_cache
|
| 116 |
+
def provider_objects(self) -> Dict[str, "Provider"]:
|
| 117 |
+
"""Return a dict of provider extension objects."""
|
| 118 |
+
self._provider_objects = self._load_entry_points(
|
| 119 |
+
self._provider_entry_points, OpenBBGroups.provider
|
| 120 |
+
)
|
| 121 |
+
return self._provider_objects
|
| 122 |
+
|
| 123 |
+
@staticmethod
|
| 124 |
+
def _sorted_entry_points(group: str) -> EntryPoints:
|
| 125 |
+
"""Return a sorted dictionary of entry points."""
|
| 126 |
+
return sorted(entry_points(group=group)) # type: ignore
|
| 127 |
+
|
| 128 |
+
def _load_entry_points(
|
| 129 |
+
self, entry_points_: EntryPoints, group: OpenBBGroups
|
| 130 |
+
) -> Dict[str, Any]:
|
| 131 |
+
"""Return a dict of objects matching the entry points."""
|
| 132 |
+
|
| 133 |
+
def load_obbject(eps: EntryPoints) -> Dict[str, Extension]:
|
| 134 |
+
"""
|
| 135 |
+
Return a dictionary of obbject objects.
|
| 136 |
+
|
| 137 |
+
Keys are entry point names and values are instances of the Extension class.
|
| 138 |
+
"""
|
| 139 |
+
return {
|
| 140 |
+
ep.name: entry
|
| 141 |
+
for ep in eps
|
| 142 |
+
if isinstance((entry := ep.load()), Extension)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
def load_core(eps: EntryPoints) -> Dict[str, "Router"]:
|
| 146 |
+
"""Return a dictionary of core objects."""
|
| 147 |
+
# pylint: disable=import-outside-toplevel
|
| 148 |
+
from openbb_core.app.router import Router
|
| 149 |
+
|
| 150 |
+
return {
|
| 151 |
+
ep.name: entry for ep in eps if isinstance((entry := ep.load()), Router)
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
def load_provider(eps: EntryPoints) -> Dict[str, "Provider"]:
|
| 155 |
+
"""
|
| 156 |
+
Return a dictionary of provider objects.
|
| 157 |
+
|
| 158 |
+
Keys are entry point names and values are instances of the Provider class.
|
| 159 |
+
"""
|
| 160 |
+
# pylint: disable=import-outside-toplevel
|
| 161 |
+
from openbb_core.provider.abstract.provider import Provider
|
| 162 |
+
|
| 163 |
+
entries: dict = {}
|
| 164 |
+
for ep in eps:
|
| 165 |
+
try:
|
| 166 |
+
if isinstance((entry := ep.load()), Provider):
|
| 167 |
+
entries[ep.name] = entry
|
| 168 |
+
except ModuleNotFoundError:
|
| 169 |
+
continue
|
| 170 |
+
return entries
|
| 171 |
+
|
| 172 |
+
func = {
|
| 173 |
+
OpenBBGroups.obbject: load_obbject,
|
| 174 |
+
OpenBBGroups.core: load_core,
|
| 175 |
+
OpenBBGroups.provider: load_provider,
|
| 176 |
+
}
|
| 177 |
+
return func[group](entry_points_) # type: ignore
|
openbb_platform/core/openbb_core/app/logs/formatters/formatter_with_exceptions.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logging Formatter that includes formatting of Exceptions."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
from openbb_core.app.logs.models.logging_settings import LoggingSettings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class FormatterWithExceptions(logging.Formatter):
|
| 9 |
+
"""Logging Formatter that includes formatting of Exceptions."""
|
| 10 |
+
|
| 11 |
+
DATEFORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
| 12 |
+
LOGFORMAT = "%(asctime)s|%(name)s|%(funcName)s|%(lineno)s|%(message)s"
|
| 13 |
+
LOGPREFIXFORMAT = (
|
| 14 |
+
"%(levelname)s|%(appName)s|%(commitHash)s|%(appId)s|%(sessionId)s|%(userId)s|"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
@staticmethod
|
| 18 |
+
def calculate_level_name(record: logging.LogRecord) -> str:
|
| 19 |
+
"""Calculate the level name of the log record."""
|
| 20 |
+
if record.exc_text:
|
| 21 |
+
level_name = "X"
|
| 22 |
+
elif record.levelname:
|
| 23 |
+
level_name = record.levelname[0]
|
| 24 |
+
else:
|
| 25 |
+
level_name = "U"
|
| 26 |
+
|
| 27 |
+
return level_name
|
| 28 |
+
|
| 29 |
+
@staticmethod
|
| 30 |
+
def extract_log_extra(record: logging.LogRecord):
|
| 31 |
+
"""Extract extra log information from the record."""
|
| 32 |
+
log_extra = dict()
|
| 33 |
+
|
| 34 |
+
if hasattr(record, "func_name_override"):
|
| 35 |
+
record.funcName = record.func_name_override # type: ignore
|
| 36 |
+
record.lineno = 0
|
| 37 |
+
|
| 38 |
+
if hasattr(record, "session_id"):
|
| 39 |
+
log_extra["sessionId"] = record.session_id # type: ignore
|
| 40 |
+
|
| 41 |
+
return log_extra
|
| 42 |
+
|
| 43 |
+
@staticmethod
|
| 44 |
+
def mock_ipv4(text: str) -> str:
|
| 45 |
+
"""Mock IPv4 addresses in the text."""
|
| 46 |
+
# pylint: disable=import-outside-toplevel
|
| 47 |
+
import re
|
| 48 |
+
|
| 49 |
+
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
|
| 50 |
+
replacement = " FILTERED_IP "
|
| 51 |
+
text_mocked = re.sub(pattern, replacement, text)
|
| 52 |
+
|
| 53 |
+
return text_mocked
|
| 54 |
+
|
| 55 |
+
@staticmethod
|
| 56 |
+
def mock_email(text: str) -> str:
|
| 57 |
+
"""Mock email addresses in the text."""
|
| 58 |
+
# pylint: disable=import-outside-toplevel
|
| 59 |
+
import re
|
| 60 |
+
|
| 61 |
+
pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
| 62 |
+
replacement = " FILTERED_EMAIL "
|
| 63 |
+
text_mocked = re.sub(pattern, replacement, text)
|
| 64 |
+
|
| 65 |
+
return text_mocked
|
| 66 |
+
|
| 67 |
+
@staticmethod
|
| 68 |
+
def mock_password(text: str) -> str:
|
| 69 |
+
"""Mock passwords in the text."""
|
| 70 |
+
# pylint: disable=import-outside-toplevel
|
| 71 |
+
import re
|
| 72 |
+
|
| 73 |
+
pattern = r'("password": ")[^"]+'
|
| 74 |
+
replacement = r"\1 FILTERED_PASSWORD "
|
| 75 |
+
text_mocked = re.sub(pattern, replacement, text)
|
| 76 |
+
return text_mocked
|
| 77 |
+
|
| 78 |
+
@staticmethod
|
| 79 |
+
def mock_flair(text: str) -> str:
|
| 80 |
+
"""Mock flair in the text."""
|
| 81 |
+
# pylint: disable=import-outside-toplevel
|
| 82 |
+
import re
|
| 83 |
+
|
| 84 |
+
pattern = r'("FLAIR": "\[)(.*?)\]'
|
| 85 |
+
replacement = r"\1 FILTERED_FLAIR ]"
|
| 86 |
+
text_mocked = re.sub(pattern, replacement, text)
|
| 87 |
+
|
| 88 |
+
return text_mocked
|
| 89 |
+
|
| 90 |
+
@staticmethod
|
| 91 |
+
def mock_home_directory(text: str) -> str:
|
| 92 |
+
"""Mock home directory in the text."""
|
| 93 |
+
# pylint: disable=import-outside-toplevel
|
| 94 |
+
from pathlib import Path
|
| 95 |
+
|
| 96 |
+
user_home_directory = str(Path.home().as_posix())
|
| 97 |
+
text_mocked = text.replace("\\", "/").replace(
|
| 98 |
+
user_home_directory, "MOCKING_USER_PATH"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
return text_mocked
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
def filter_special_tags(text: str) -> str:
|
| 105 |
+
"""Filter special tags in the text."""
|
| 106 |
+
text_filtered = text.replace("\n", " MOCKING_BREAKLINE ")
|
| 107 |
+
text_filtered = text_filtered.replace("'Traceback", "Traceback")
|
| 108 |
+
|
| 109 |
+
return text_filtered
|
| 110 |
+
|
| 111 |
+
@classmethod
|
| 112 |
+
def filter_piis(cls, text: str) -> str:
|
| 113 |
+
"""Filter Personally Identifiable Information in the text."""
|
| 114 |
+
text_filtered = cls.mock_ipv4(text=text)
|
| 115 |
+
text_filtered = cls.mock_email(text=text_filtered)
|
| 116 |
+
text_filtered = cls.mock_password(text=text_filtered)
|
| 117 |
+
text_filtered = cls.mock_home_directory(text=text_filtered)
|
| 118 |
+
text_filtered = cls.mock_flair(text=text_filtered)
|
| 119 |
+
|
| 120 |
+
return text_filtered
|
| 121 |
+
|
| 122 |
+
@classmethod
|
| 123 |
+
def filter_log_line(cls, text: str):
|
| 124 |
+
"""Filter log line."""
|
| 125 |
+
text_filtered = cls.filter_special_tags(text=text)
|
| 126 |
+
text_filtered = cls.filter_piis(text=text_filtered)
|
| 127 |
+
|
| 128 |
+
return text_filtered
|
| 129 |
+
|
| 130 |
+
# OVERRIDE
|
| 131 |
+
def __init__(
|
| 132 |
+
self,
|
| 133 |
+
settings: LoggingSettings,
|
| 134 |
+
style="%",
|
| 135 |
+
validate=True,
|
| 136 |
+
) -> None:
|
| 137 |
+
"""Initialize the FormatterWithExceptions."""
|
| 138 |
+
super().__init__(
|
| 139 |
+
fmt=self.LOGFORMAT,
|
| 140 |
+
datefmt=self.DATEFORMAT,
|
| 141 |
+
style=style,
|
| 142 |
+
validate=validate,
|
| 143 |
+
)
|
| 144 |
+
self.settings = settings
|
| 145 |
+
|
| 146 |
+
@property
|
| 147 |
+
def settings(self) -> LoggingSettings:
|
| 148 |
+
"""Get the settings."""
|
| 149 |
+
# pylint: disable=import-outside-toplevel
|
| 150 |
+
from copy import deepcopy
|
| 151 |
+
|
| 152 |
+
return deepcopy(self.__settings)
|
| 153 |
+
|
| 154 |
+
@settings.setter
|
| 155 |
+
def settings(self, settings: LoggingSettings) -> None:
|
| 156 |
+
"""Set the settings."""
|
| 157 |
+
self.__settings = settings
|
| 158 |
+
|
| 159 |
+
# OVERRIDE
|
| 160 |
+
def formatException(self, ei) -> str:
|
| 161 |
+
"""Define the Exception formatting handler.
|
| 162 |
+
|
| 163 |
+
Parameters
|
| 164 |
+
----------
|
| 165 |
+
ei : logging._SysExcInfoType
|
| 166 |
+
Exception to be logged
|
| 167 |
+
Returns
|
| 168 |
+
----------
|
| 169 |
+
str
|
| 170 |
+
Formatted exception
|
| 171 |
+
"""
|
| 172 |
+
result = super().formatException(ei)
|
| 173 |
+
return repr(result)
|
| 174 |
+
|
| 175 |
+
# OVERRIDE
|
| 176 |
+
def format(self, record: logging.LogRecord) -> str:
|
| 177 |
+
"""Define the Log formatter.
|
| 178 |
+
|
| 179 |
+
Parameters
|
| 180 |
+
----------
|
| 181 |
+
record : logging.LogRecord
|
| 182 |
+
Logging record
|
| 183 |
+
Returns
|
| 184 |
+
----------
|
| 185 |
+
str
|
| 186 |
+
Formatted_log message
|
| 187 |
+
"""
|
| 188 |
+
level_name = self.calculate_level_name(record=record)
|
| 189 |
+
log_prefix_content = {
|
| 190 |
+
"appName": self.settings.app_name,
|
| 191 |
+
"levelname": level_name,
|
| 192 |
+
"appId": self.settings.app_id,
|
| 193 |
+
"sessionId": self.settings.session_id,
|
| 194 |
+
"commitHash": "unknown-commit",
|
| 195 |
+
"userId": self.settings.user_id,
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
log_extra = self.extract_log_extra(record=record)
|
| 199 |
+
log_prefix_content = {**log_prefix_content, **log_extra}
|
| 200 |
+
log_prefix = self.LOGPREFIXFORMAT % log_prefix_content
|
| 201 |
+
|
| 202 |
+
record.msg = record.msg.replace("|", "-MOCK_PIPE-")
|
| 203 |
+
|
| 204 |
+
log_line = super().format(record)
|
| 205 |
+
log_line = self.filter_log_line(text=log_line)
|
| 206 |
+
log_line_full = log_prefix + log_line
|
| 207 |
+
|
| 208 |
+
return log_line_full
|
openbb_platform/core/openbb_core/app/logs/handlers/path_tracking_file_handler.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Path Tracking File Handler."""
|
| 2 |
+
|
| 3 |
+
# IMPORTATION STANDARD
|
| 4 |
+
from copy import deepcopy
|
| 5 |
+
from logging.handlers import TimedRotatingFileHandler
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
# IMPORTATION THIRD PARTY
|
| 9 |
+
# IMPORTATION INTERNAL
|
| 10 |
+
from openbb_core.app.logs.models.logging_settings import LoggingSettings
|
| 11 |
+
from openbb_core.app.logs.utils.expired_files import (
|
| 12 |
+
get_expired_file_list,
|
| 13 |
+
get_timestamp_from_x_days,
|
| 14 |
+
remove_file_list,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
ARCHIVES_FOLDER_NAME = "archives"
|
| 18 |
+
TMP_FOLDER_NAME = "tmp"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class PathTrackingFileHandler(TimedRotatingFileHandler):
|
| 22 |
+
"""Path Tracking File Handler."""
|
| 23 |
+
|
| 24 |
+
@staticmethod
|
| 25 |
+
def build_log_file_path(settings: LoggingSettings) -> Path:
|
| 26 |
+
"""Build the log file path."""
|
| 27 |
+
app_name = settings.app_name
|
| 28 |
+
directory = settings.user_logs_directory
|
| 29 |
+
session_id = settings.session_id
|
| 30 |
+
|
| 31 |
+
path = directory.absolute().joinpath(f"{app_name}_{session_id}")
|
| 32 |
+
return path
|
| 33 |
+
|
| 34 |
+
def clean_expired_files(self, before_timestamp: float):
|
| 35 |
+
"""Remove expired files from logs directory."""
|
| 36 |
+
logs_dir = self.settings.user_logs_directory
|
| 37 |
+
archives_directory = logs_dir / ARCHIVES_FOLDER_NAME
|
| 38 |
+
tmp_directory = logs_dir / TMP_FOLDER_NAME
|
| 39 |
+
|
| 40 |
+
expired_logs_file_list = get_expired_file_list(
|
| 41 |
+
directory=logs_dir,
|
| 42 |
+
before_timestamp=before_timestamp,
|
| 43 |
+
)
|
| 44 |
+
expired_archives_file_list = get_expired_file_list(
|
| 45 |
+
directory=archives_directory,
|
| 46 |
+
before_timestamp=before_timestamp,
|
| 47 |
+
)
|
| 48 |
+
expired_tmp_file_list = get_expired_file_list(
|
| 49 |
+
directory=tmp_directory,
|
| 50 |
+
before_timestamp=before_timestamp,
|
| 51 |
+
)
|
| 52 |
+
remove_file_list(file_list=expired_logs_file_list)
|
| 53 |
+
remove_file_list(file_list=expired_archives_file_list)
|
| 54 |
+
remove_file_list(file_list=expired_tmp_file_list)
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def settings(self) -> LoggingSettings:
|
| 58 |
+
"""Get the settings."""
|
| 59 |
+
return deepcopy(self.__settings)
|
| 60 |
+
|
| 61 |
+
@settings.setter
|
| 62 |
+
def settings(self, settings: LoggingSettings) -> None:
|
| 63 |
+
"""Set the settings."""
|
| 64 |
+
self.__settings = settings
|
| 65 |
+
|
| 66 |
+
# OVERRIDE
|
| 67 |
+
def __init__(
|
| 68 |
+
self,
|
| 69 |
+
settings: LoggingSettings,
|
| 70 |
+
*args,
|
| 71 |
+
**kwargs,
|
| 72 |
+
) -> None:
|
| 73 |
+
"""Initialize the PathTrackingFileHandler."""
|
| 74 |
+
# SETUP PARENT CLASS
|
| 75 |
+
filename = str(self.build_log_file_path(settings=settings))
|
| 76 |
+
frequency = settings.frequency
|
| 77 |
+
kwargs["when"] = frequency
|
| 78 |
+
|
| 79 |
+
super().__init__(filename, *args, **kwargs)
|
| 80 |
+
|
| 81 |
+
self.suffix += ".log"
|
| 82 |
+
|
| 83 |
+
# SETUP CURRENT CLASS
|
| 84 |
+
self.__settings = settings
|
| 85 |
+
|
| 86 |
+
self.clean_expired_files(before_timestamp=get_timestamp_from_x_days(x=5))
|
openbb_platform/core/openbb_core/app/logs/handlers/posthog_handler.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Posthog Handler."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from typing import Any, Dict
|
| 7 |
+
|
| 8 |
+
import posthog
|
| 9 |
+
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
|
| 10 |
+
FormatterWithExceptions,
|
| 11 |
+
)
|
| 12 |
+
from openbb_core.app.logs.models.logging_settings import LoggingSettings
|
| 13 |
+
from openbb_core.env import Env
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class PosthogHandler(logging.Handler):
|
| 17 |
+
"""Posthog Handler."""
|
| 18 |
+
|
| 19 |
+
class AllowedEvents(Enum):
|
| 20 |
+
"""Allowed Posthog Events."""
|
| 21 |
+
|
| 22 |
+
log_startup = "log_startup"
|
| 23 |
+
log_cmd = "log_cmd"
|
| 24 |
+
log_error = "log_error"
|
| 25 |
+
log_warning = "log_warning"
|
| 26 |
+
|
| 27 |
+
def __init__(self, settings: LoggingSettings):
|
| 28 |
+
"""Initialize Posthog Handler."""
|
| 29 |
+
# pylint: disable=import-outside-toplevel
|
| 30 |
+
from openbb_core.provider.utils.helpers import get_requests_session
|
| 31 |
+
|
| 32 |
+
super().__init__()
|
| 33 |
+
self._settings = settings
|
| 34 |
+
self.logged_in = False
|
| 35 |
+
posthog.api_key = "phc_6FXLqu4uW9yxfyN8DpPdgzCdlYXOmIWdMGh6GnBgJLX" # pragma: allowlist secret # noqa
|
| 36 |
+
posthog.host = "https://app.posthog.com" # noqa
|
| 37 |
+
posthog.request._session = ( # pylint: disable=protected-access
|
| 38 |
+
get_requests_session()
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def settings(self) -> LoggingSettings:
|
| 43 |
+
"""Get logging settings."""
|
| 44 |
+
# pylint: disable=import-outside-toplevel
|
| 45 |
+
from copy import deepcopy
|
| 46 |
+
|
| 47 |
+
return deepcopy(self._settings)
|
| 48 |
+
|
| 49 |
+
@settings.setter
|
| 50 |
+
def settings(self, settings: LoggingSettings) -> None:
|
| 51 |
+
"""Set logging settings."""
|
| 52 |
+
self._settings = settings
|
| 53 |
+
|
| 54 |
+
def emit(self, record: logging.LogRecord):
|
| 55 |
+
"""Emit log record."""
|
| 56 |
+
try:
|
| 57 |
+
self.send(record=record)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
self.handleError(record)
|
| 60 |
+
if Env().DEBUG_MODE:
|
| 61 |
+
raise e
|
| 62 |
+
|
| 63 |
+
def distinct_id(self) -> str:
|
| 64 |
+
"""Get distinct id."""
|
| 65 |
+
return self._settings.user_id or self._settings.app_id
|
| 66 |
+
|
| 67 |
+
def identify(self) -> None:
|
| 68 |
+
"""Identify user."""
|
| 69 |
+
if self.logged_in or not self._settings.user_id:
|
| 70 |
+
return
|
| 71 |
+
|
| 72 |
+
posthog.identify(
|
| 73 |
+
self._settings.user_id,
|
| 74 |
+
{
|
| 75 |
+
"email": self._settings.user_email,
|
| 76 |
+
"primaryUsage": self._settings.user_primary_usage,
|
| 77 |
+
},
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
if self._settings.sub_app_name == "pro":
|
| 81 |
+
return
|
| 82 |
+
|
| 83 |
+
self.logged_in = True
|
| 84 |
+
posthog.alias(self._settings.user_id, self._settings.app_id)
|
| 85 |
+
|
| 86 |
+
def log_to_dict(self, log_info: str) -> dict:
|
| 87 |
+
"""Log to dict."""
|
| 88 |
+
# pylint: disable=import-outside-toplevel
|
| 89 |
+
import re
|
| 90 |
+
|
| 91 |
+
log_regex = r"(STARTUP|CMD|ERROR): (.*)"
|
| 92 |
+
log_dict: Dict[str, Any] = {}
|
| 93 |
+
|
| 94 |
+
for log in re.findall(log_regex, log_info):
|
| 95 |
+
log_dict[log[0]] = json.loads(log[1])
|
| 96 |
+
|
| 97 |
+
return log_dict
|
| 98 |
+
|
| 99 |
+
def send(self, record: logging.LogRecord):
|
| 100 |
+
"""Send log record to Posthog."""
|
| 101 |
+
# pylint: disable=import-outside-toplevel
|
| 102 |
+
import re
|
| 103 |
+
|
| 104 |
+
level_name = logging.getLevelName(record.levelno)
|
| 105 |
+
log_line = FormatterWithExceptions.filter_log_line(text=record.getMessage())
|
| 106 |
+
|
| 107 |
+
log_extra = self.extract_log_extra(record=record)
|
| 108 |
+
log_extra.update(dict(level=level_name, message=log_line))
|
| 109 |
+
event_name = f"log_{level_name.lower()}"
|
| 110 |
+
|
| 111 |
+
if log_dict := self.log_to_dict(log_info=log_line):
|
| 112 |
+
event_name = f"log_{list(log_dict.keys())[0].lower()}"
|
| 113 |
+
log_dict = log_dict.get("STARTUP", log_dict)
|
| 114 |
+
|
| 115 |
+
log_extra = {**log_extra, **log_dict}
|
| 116 |
+
log_extra.pop("message", None)
|
| 117 |
+
|
| 118 |
+
if re.match(r"^(QUEUE|START|END|INPUT:)", log_line) and not log_dict:
|
| 119 |
+
return
|
| 120 |
+
|
| 121 |
+
if event_name not in [e.value for e in self.AllowedEvents]:
|
| 122 |
+
return
|
| 123 |
+
|
| 124 |
+
self.identify()
|
| 125 |
+
posthog.capture(self.distinct_id(), event_name, properties=log_extra)
|
| 126 |
+
|
| 127 |
+
def extract_log_extra(self, record: logging.LogRecord) -> Dict[str, Any]:
|
| 128 |
+
"""Extract log extra from record."""
|
| 129 |
+
log_extra: Dict[str, Any] = {
|
| 130 |
+
"appName": self._settings.app_name,
|
| 131 |
+
"subAppName": self._settings.sub_app_name,
|
| 132 |
+
"appId": self._settings.app_id,
|
| 133 |
+
"sessionId": self._settings.session_id,
|
| 134 |
+
"platform": self._settings.platform,
|
| 135 |
+
"pythonVersion": self._settings.python_version,
|
| 136 |
+
"obbPlatformVersion": self._settings.platform_version,
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if self._settings.user_id:
|
| 140 |
+
log_extra["userId"] = self._settings.user_id
|
| 141 |
+
|
| 142 |
+
if hasattr(record, "extra"):
|
| 143 |
+
log_extra = {**log_extra, **record.extra} # type: ignore
|
| 144 |
+
|
| 145 |
+
if record.exc_info:
|
| 146 |
+
log_extra["exception"] = {
|
| 147 |
+
"type": str(record.exc_info[0]),
|
| 148 |
+
"value": str(record.exc_info[1]),
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
return log_extra
|
openbb_platform/core/openbb_core/app/logs/handlers_manager.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Handlers Manager."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
|
| 7 |
+
FormatterWithExceptions,
|
| 8 |
+
)
|
| 9 |
+
from openbb_core.app.logs.handlers.path_tracking_file_handler import (
|
| 10 |
+
PathTrackingFileHandler,
|
| 11 |
+
)
|
| 12 |
+
from openbb_core.app.logs.handlers.posthog_handler import PosthogHandler
|
| 13 |
+
from openbb_core.app.logs.models.logging_settings import LoggingSettings
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class HandlersManager:
|
| 17 |
+
"""Handlers Manager."""
|
| 18 |
+
|
| 19 |
+
def __init__(self, logger: logging.Logger, settings: LoggingSettings):
|
| 20 |
+
"""Initialize the HandlersManager."""
|
| 21 |
+
self._logger = logger
|
| 22 |
+
self._handlers = settings.handler_list
|
| 23 |
+
self._settings = settings
|
| 24 |
+
|
| 25 |
+
def setup(self):
|
| 26 |
+
"""Set the logger handlers and settings."""
|
| 27 |
+
# Disable propagation to root logger to avoid duplicate logs
|
| 28 |
+
self._logger.propagate = False
|
| 29 |
+
self._logger.setLevel(self._settings.verbosity)
|
| 30 |
+
|
| 31 |
+
for handler_type in self._handlers:
|
| 32 |
+
if handler_type == "stdout":
|
| 33 |
+
self._add_stdout_handler()
|
| 34 |
+
elif handler_type == "stderr":
|
| 35 |
+
self._add_stderr_handler()
|
| 36 |
+
elif handler_type == "noop":
|
| 37 |
+
self._add_noop_handler()
|
| 38 |
+
elif handler_type == "file" and not self._settings.logging_suppress:
|
| 39 |
+
self._add_file_handler()
|
| 40 |
+
elif handler_type == "posthog" and not self._settings.logging_suppress:
|
| 41 |
+
self._add_posthog_handler()
|
| 42 |
+
else:
|
| 43 |
+
self._logger.debug("Unknown log handler.")
|
| 44 |
+
|
| 45 |
+
def _add_posthog_handler(self):
|
| 46 |
+
"""Add a Posthog handler."""
|
| 47 |
+
handler = PosthogHandler(settings=self._settings)
|
| 48 |
+
formatter = FormatterWithExceptions(settings=self._settings)
|
| 49 |
+
handler.setFormatter(formatter)
|
| 50 |
+
self._logger.addHandler(handler)
|
| 51 |
+
|
| 52 |
+
def _add_stdout_handler(self):
|
| 53 |
+
"""Add a stdout handler."""
|
| 54 |
+
handler = logging.StreamHandler(sys.stdout)
|
| 55 |
+
formatter = FormatterWithExceptions(settings=self._settings)
|
| 56 |
+
handler.setFormatter(formatter)
|
| 57 |
+
self._logger.addHandler(handler)
|
| 58 |
+
|
| 59 |
+
def _add_stderr_handler(self):
|
| 60 |
+
"""Add a stderr handler."""
|
| 61 |
+
handler = logging.StreamHandler(sys.stderr)
|
| 62 |
+
formatter = FormatterWithExceptions(settings=self._settings)
|
| 63 |
+
handler.setFormatter(formatter)
|
| 64 |
+
self._logger.addHandler(handler)
|
| 65 |
+
|
| 66 |
+
def _add_noop_handler(self):
|
| 67 |
+
"""Add a null handler."""
|
| 68 |
+
handler = logging.NullHandler()
|
| 69 |
+
formatter = FormatterWithExceptions(settings=self._settings)
|
| 70 |
+
handler.setFormatter(formatter)
|
| 71 |
+
self._logger.addHandler(handler)
|
| 72 |
+
|
| 73 |
+
def _add_file_handler(self):
|
| 74 |
+
"""Add a file handler."""
|
| 75 |
+
handler = PathTrackingFileHandler(settings=self._settings)
|
| 76 |
+
formatter = FormatterWithExceptions(settings=self._settings)
|
| 77 |
+
handler.setFormatter(formatter)
|
| 78 |
+
self._logger.addHandler(handler)
|
| 79 |
+
|
| 80 |
+
def update_handlers(self, settings: LoggingSettings):
|
| 81 |
+
"""Update the handlers with new settings."""
|
| 82 |
+
logger = self._logger
|
| 83 |
+
for hdlr in logger.handlers:
|
| 84 |
+
if (
|
| 85 |
+
isinstance(hdlr, (PathTrackingFileHandler, PosthogHandler))
|
| 86 |
+
and not settings.logging_suppress
|
| 87 |
+
):
|
| 88 |
+
hdlr.settings = settings
|
| 89 |
+
hdlr.formatter.settings = settings # type: ignore
|
openbb_platform/core/openbb_core/app/logs/logging_service.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logging Service Module."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from types import TracebackType
|
| 7 |
+
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
|
| 8 |
+
|
| 9 |
+
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
|
| 10 |
+
FormatterWithExceptions,
|
| 11 |
+
)
|
| 12 |
+
from openbb_core.app.logs.handlers_manager import HandlersManager
|
| 13 |
+
from openbb_core.app.logs.models.logging_settings import LoggingSettings
|
| 14 |
+
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
| 15 |
+
from openbb_core.app.model.system_settings import SystemSettings
|
| 16 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 17 |
+
from pydantic import BaseModel
|
| 18 |
+
from pydantic_core import to_jsonable_python
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class DummyProvider(BaseModel):
|
| 22 |
+
"""Dummy Provider for error handling with logs."""
|
| 23 |
+
|
| 24 |
+
provider: str = "not_passed_to_kwargs"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class LoggingService(metaclass=SingletonMeta):
|
| 28 |
+
"""Logging Service class responsible for managing logging settings and handling logs.
|
| 29 |
+
|
| 30 |
+
Attributes
|
| 31 |
+
----------
|
| 32 |
+
_user_settings : Optional[UserSettings]
|
| 33 |
+
User Settings object.
|
| 34 |
+
_system_settings : Optional[SystemSettings]
|
| 35 |
+
System Settings object.
|
| 36 |
+
_logging_settings : LoggingSettings
|
| 37 |
+
LoggingSettings object containing the current logging settings.
|
| 38 |
+
_handlers_manager : HandlersManager
|
| 39 |
+
HandlersManager object managing logging handlers.
|
| 40 |
+
|
| 41 |
+
Methods
|
| 42 |
+
-------
|
| 43 |
+
__init__(system_settings, user_settings)
|
| 44 |
+
Logging Manager Constructor.
|
| 45 |
+
|
| 46 |
+
log(user_settings, system_settings, obbject, route, func, kwargs)
|
| 47 |
+
Log command output and relevant information.
|
| 48 |
+
|
| 49 |
+
logging_settings
|
| 50 |
+
Property to access the current logging settings.
|
| 51 |
+
|
| 52 |
+
logging_settings.setter
|
| 53 |
+
Setter method to update the logging settings.
|
| 54 |
+
|
| 55 |
+
_setup_handlers()
|
| 56 |
+
Setup Logging Handlers.
|
| 57 |
+
|
| 58 |
+
_log_startup()
|
| 59 |
+
Log startup information.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
_logger = logging.getLogger("openbb.logging_service")
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
system_settings: SystemSettings,
|
| 67 |
+
user_settings: UserSettings,
|
| 68 |
+
) -> None:
|
| 69 |
+
"""Define the Logging Service Constructor.
|
| 70 |
+
|
| 71 |
+
Sets up the logging settings and handlers and then logs the startup information.
|
| 72 |
+
|
| 73 |
+
Parameters
|
| 74 |
+
----------
|
| 75 |
+
system_settings : SystemSettings
|
| 76 |
+
System Settings, by default None
|
| 77 |
+
user_settings : UserSettings
|
| 78 |
+
User Settings, by default None
|
| 79 |
+
"""
|
| 80 |
+
self._user_settings = user_settings
|
| 81 |
+
self._system_settings = system_settings
|
| 82 |
+
self._logging_settings = LoggingSettings(
|
| 83 |
+
user_settings=self._user_settings,
|
| 84 |
+
system_settings=self._system_settings,
|
| 85 |
+
)
|
| 86 |
+
self._handlers_manager = self._setup_handlers()
|
| 87 |
+
self._log_startup()
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def logging_settings(self) -> LoggingSettings:
|
| 91 |
+
"""Define the Current logging settings.
|
| 92 |
+
|
| 93 |
+
Returns
|
| 94 |
+
-------
|
| 95 |
+
LoggingSettings
|
| 96 |
+
LoggingSettings object containing the current logging settings.
|
| 97 |
+
"""
|
| 98 |
+
return self._logging_settings
|
| 99 |
+
|
| 100 |
+
@logging_settings.setter
|
| 101 |
+
def logging_settings(self, value: Tuple[SystemSettings, UserSettings]):
|
| 102 |
+
"""Define the Setter for updating the logging settings.
|
| 103 |
+
|
| 104 |
+
Parameters
|
| 105 |
+
----------
|
| 106 |
+
value : Tuple[SystemSettings, UserSettings]
|
| 107 |
+
Tuple containing updated SystemSettings and UserSettings.
|
| 108 |
+
"""
|
| 109 |
+
system_settings, user_settings = value
|
| 110 |
+
self._logging_settings = LoggingSettings(
|
| 111 |
+
user_settings=user_settings,
|
| 112 |
+
system_settings=system_settings,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def _setup_handlers(self) -> HandlersManager:
|
| 116 |
+
"""Set up Logging Handlers.
|
| 117 |
+
|
| 118 |
+
Returns
|
| 119 |
+
-------
|
| 120 |
+
HandlersManager
|
| 121 |
+
Handlers Manager object.
|
| 122 |
+
"""
|
| 123 |
+
handlers_manager = HandlersManager(
|
| 124 |
+
self._logger, settings=self._logging_settings
|
| 125 |
+
)
|
| 126 |
+
handlers_manager.setup()
|
| 127 |
+
|
| 128 |
+
self._logger.info("Logging configuration finished")
|
| 129 |
+
self._logger.info("Logging set to %s", self._logging_settings.handler_list)
|
| 130 |
+
self._logger.info("Verbosity set to %s", self._logging_settings.verbosity)
|
| 131 |
+
self._logger.info(
|
| 132 |
+
"LOGFORMAT: %s%s",
|
| 133 |
+
FormatterWithExceptions.LOGPREFIXFORMAT.replace("|", "-"),
|
| 134 |
+
FormatterWithExceptions.LOGFORMAT.replace("|", "-"),
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
return handlers_manager
|
| 138 |
+
|
| 139 |
+
def _log_startup(
|
| 140 |
+
self,
|
| 141 |
+
route: Optional[str] = None,
|
| 142 |
+
custom_headers: Optional[Dict[str, Any]] = None,
|
| 143 |
+
) -> None:
|
| 144 |
+
"""Log startup information."""
|
| 145 |
+
|
| 146 |
+
def check_credentials_defined(credentials: Dict[str, Any]):
|
| 147 |
+
class CredentialsDefinition(Enum):
|
| 148 |
+
defined = "defined"
|
| 149 |
+
undefined = "undefined"
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
c: (
|
| 153 |
+
CredentialsDefinition.defined.value
|
| 154 |
+
if credentials[c]
|
| 155 |
+
else CredentialsDefinition.undefined.value
|
| 156 |
+
)
|
| 157 |
+
for c in credentials
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
self._logger.info(
|
| 161 |
+
"STARTUP: %s ",
|
| 162 |
+
json.dumps(
|
| 163 |
+
{
|
| 164 |
+
"route": route,
|
| 165 |
+
"PREFERENCES": self._user_settings.preferences,
|
| 166 |
+
"KEYS": check_credentials_defined(
|
| 167 |
+
self._user_settings.credentials.model_dump()
|
| 168 |
+
if self._user_settings.credentials
|
| 169 |
+
else {}
|
| 170 |
+
),
|
| 171 |
+
"SYSTEM": self._system_settings,
|
| 172 |
+
"custom_headers": custom_headers,
|
| 173 |
+
},
|
| 174 |
+
default=to_jsonable_python,
|
| 175 |
+
),
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# pylint: disable=R0917
|
| 179 |
+
def log(
|
| 180 |
+
self,
|
| 181 |
+
user_settings: UserSettings,
|
| 182 |
+
system_settings: SystemSettings,
|
| 183 |
+
route: str,
|
| 184 |
+
func: Callable,
|
| 185 |
+
kwargs: Dict[str, Any],
|
| 186 |
+
exec_info: Union[
|
| 187 |
+
Tuple[Type[BaseException], BaseException, TracebackType],
|
| 188 |
+
Tuple[None, None, None],
|
| 189 |
+
],
|
| 190 |
+
custom_headers: Optional[Dict[str, Any]] = None,
|
| 191 |
+
):
|
| 192 |
+
"""Log command output and relevant information.
|
| 193 |
+
|
| 194 |
+
Parameters
|
| 195 |
+
----------
|
| 196 |
+
user_settings : UserSettings
|
| 197 |
+
User Settings object.
|
| 198 |
+
system_settings : SystemSettings
|
| 199 |
+
System Settings object.
|
| 200 |
+
route : str
|
| 201 |
+
Route for the command.
|
| 202 |
+
func : Callable
|
| 203 |
+
Callable representing the executed function.
|
| 204 |
+
kwargs : Dict[str, Any]
|
| 205 |
+
Keyword arguments passed to the function.
|
| 206 |
+
exec_info : Union[
|
| 207 |
+
Tuple[Type[BaseException], BaseException, TracebackType],
|
| 208 |
+
Tuple[None, None, None],
|
| 209 |
+
]
|
| 210 |
+
Exception information, by default None
|
| 211 |
+
"""
|
| 212 |
+
self._user_settings = user_settings
|
| 213 |
+
self._system_settings = system_settings
|
| 214 |
+
self._logging_settings = LoggingSettings(
|
| 215 |
+
user_settings=self._user_settings,
|
| 216 |
+
system_settings=self._system_settings,
|
| 217 |
+
)
|
| 218 |
+
self._handlers_manager.update_handlers(self._logging_settings)
|
| 219 |
+
|
| 220 |
+
if not self._logging_settings.logging_suppress:
|
| 221 |
+
|
| 222 |
+
if "login" in route:
|
| 223 |
+
self._log_startup(route, custom_headers)
|
| 224 |
+
else:
|
| 225 |
+
|
| 226 |
+
# Remove CommandContext if any
|
| 227 |
+
kwargs.pop("cc", None)
|
| 228 |
+
|
| 229 |
+
# Get provider for posthog logs
|
| 230 |
+
passed_model = kwargs.get("provider_choices", DummyProvider())
|
| 231 |
+
provider = (
|
| 232 |
+
passed_model.provider
|
| 233 |
+
if hasattr(passed_model, "provider")
|
| 234 |
+
else "not_passed_to_kwargs"
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
# Truncate kwargs if too long
|
| 238 |
+
kwargs = {k: str(v)[:300] for k, v in kwargs.items()}
|
| 239 |
+
# Get execution info
|
| 240 |
+
error = None if all(i is None for i in exec_info) else str(exec_info[1])
|
| 241 |
+
|
| 242 |
+
# Construct message
|
| 243 |
+
message_label = "ERROR" if error else "CMD"
|
| 244 |
+
log_message = json.dumps(
|
| 245 |
+
{
|
| 246 |
+
"route": route,
|
| 247 |
+
"input": kwargs,
|
| 248 |
+
"error": error,
|
| 249 |
+
"provider": provider,
|
| 250 |
+
"custom_headers": custom_headers,
|
| 251 |
+
},
|
| 252 |
+
default=to_jsonable_python,
|
| 253 |
+
)
|
| 254 |
+
log_message = f"{message_label}: {log_message}"
|
| 255 |
+
log_level = self._logger.error if error else self._logger.info
|
| 256 |
+
log_level(
|
| 257 |
+
log_message,
|
| 258 |
+
extra={"func_name_override": func.__name__},
|
| 259 |
+
exc_info=exec_info,
|
| 260 |
+
)
|
openbb_platform/core/openbb_core/app/logs/models/logging_settings.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logging settings."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
|
| 6 |
+
from openbb_core.app.logs.utils.utils import get_app_id, get_log_dir, get_session_id
|
| 7 |
+
from openbb_core.app.model.system_settings import SystemSettings
|
| 8 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# pylint: disable=too-many-instance-attributes
|
| 12 |
+
class LoggingSettings:
|
| 13 |
+
"""Logging settings."""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
user_settings: Optional[UserSettings] = None,
|
| 18 |
+
system_settings: Optional[SystemSettings] = None,
|
| 19 |
+
):
|
| 20 |
+
"""Initialize the logging settings."""
|
| 21 |
+
user_settings = user_settings if user_settings is not None else UserSettings()
|
| 22 |
+
system_settings = (
|
| 23 |
+
system_settings if system_settings is not None else SystemSettings()
|
| 24 |
+
)
|
| 25 |
+
user_data_directory = (
|
| 26 |
+
str(Path.home() / "OpenBBUserData")
|
| 27 |
+
if not user_settings.preferences
|
| 28 |
+
else user_settings.preferences.data_directory
|
| 29 |
+
)
|
| 30 |
+
hub_session = (
|
| 31 |
+
user_settings.profile.hub_session if user_settings.profile else None
|
| 32 |
+
)
|
| 33 |
+
if hub_session:
|
| 34 |
+
user_id = hub_session.user_uuid
|
| 35 |
+
user_email = hub_session.email
|
| 36 |
+
user_primary_usage = hub_session.primary_usage
|
| 37 |
+
else:
|
| 38 |
+
user_id, user_email, user_primary_usage = None, None, None
|
| 39 |
+
|
| 40 |
+
# System
|
| 41 |
+
self.app_name: str = system_settings.logging_app_name
|
| 42 |
+
self.sub_app_name: str = system_settings.logging_sub_app
|
| 43 |
+
self.app_id: str = get_app_id(user_data_directory)
|
| 44 |
+
self.session_id: str = get_session_id()
|
| 45 |
+
self.frequency: str = system_settings.logging_frequency
|
| 46 |
+
self.handler_list: List[str] = system_settings.logging_handlers
|
| 47 |
+
self.rolling_clock: bool = system_settings.logging_rolling_clock
|
| 48 |
+
self.verbosity: int = system_settings.logging_verbosity
|
| 49 |
+
self.platform: str = system_settings.platform
|
| 50 |
+
self.python_version: str = system_settings.python_version
|
| 51 |
+
self.platform_version: str = system_settings.version
|
| 52 |
+
self.logging_suppress: bool = system_settings.logging_suppress
|
| 53 |
+
self.log_collect: bool = system_settings.log_collect
|
| 54 |
+
# User
|
| 55 |
+
self.user_id: Optional[str] = user_id
|
| 56 |
+
self.user_logs_directory: Path = get_log_dir(user_data_directory)
|
| 57 |
+
self.user_email: Optional[str] = user_email
|
| 58 |
+
self.user_primary_usage: Optional[str] = user_primary_usage
|
openbb_platform/core/openbb_core/app/logs/utils/expired_files.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Expired files management utilities."""
|
| 2 |
+
|
| 3 |
+
import contextlib
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import List
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_timestamp_from_x_days(x: int) -> float:
|
| 10 |
+
"""Get the timestamp from x days ago."""
|
| 11 |
+
timestamp_from_x_days = datetime.now().timestamp() - x * 86400
|
| 12 |
+
return timestamp_from_x_days
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_expired_file_list(directory: Path, before_timestamp: float) -> List[Path]:
|
| 16 |
+
"""Get the list of expired files from a directory."""
|
| 17 |
+
expired_files = []
|
| 18 |
+
if directory.is_dir(): # Check if the directory exists and is a directory
|
| 19 |
+
for file in directory.iterdir():
|
| 20 |
+
if file.is_file() and file.lstat().st_mtime < before_timestamp:
|
| 21 |
+
expired_files.append(file)
|
| 22 |
+
|
| 23 |
+
return expired_files
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def remove_file_list(file_list: List[Path]):
|
| 27 |
+
"""Remove a list of files."""
|
| 28 |
+
for file in file_list:
|
| 29 |
+
with contextlib.suppress(PermissionError):
|
| 30 |
+
file.unlink(missing_ok=True)
|
openbb_platform/core/openbb_core/app/logs/utils/utils.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility functions for logging."""
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
import uuid
|
| 5 |
+
import warnings
|
| 6 |
+
from pathlib import Path, PosixPath
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_session_id() -> str:
|
| 10 |
+
"""UUID of the current session."""
|
| 11 |
+
session_id = str(uuid.uuid4()) + "-" + str(int(time.time()))
|
| 12 |
+
return session_id
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_app_id(contextual_user_data_directory: str) -> str:
|
| 16 |
+
"""Get UUID of the current installation."""
|
| 17 |
+
try:
|
| 18 |
+
app_id = get_log_dir(contextual_user_data_directory).stem
|
| 19 |
+
except OSError as e:
|
| 20 |
+
if e.errno == 30:
|
| 21 |
+
warnings.warn("Please move the application into a writable location.")
|
| 22 |
+
warnings.warn(
|
| 23 |
+
"Note for macOS users: copy `OpenBB Terminal` folder outside the DMG."
|
| 24 |
+
)
|
| 25 |
+
raise e
|
| 26 |
+
except Exception as e:
|
| 27 |
+
raise e
|
| 28 |
+
|
| 29 |
+
return app_id
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_log_dir(contextual_user_data_directory: str) -> PosixPath:
|
| 33 |
+
"""Retrieve application's log directory."""
|
| 34 |
+
log_dir = create_log_dir_if_not_exists(contextual_user_data_directory)
|
| 35 |
+
logging_uuid = create_log_uuid_if_not_exists(log_dir)
|
| 36 |
+
uuid_log_dir = create_uuid_dir_if_not_exists(log_dir, logging_uuid)
|
| 37 |
+
|
| 38 |
+
return uuid_log_dir
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def create_log_dir_if_not_exists(contextual_user_data_directory: str) -> Path:
|
| 42 |
+
"""Create a log directory for the current installation."""
|
| 43 |
+
log_dir = Path(contextual_user_data_directory).joinpath("logs").absolute()
|
| 44 |
+
if not log_dir.is_dir():
|
| 45 |
+
log_dir.mkdir(parents=True, exist_ok=True)
|
| 46 |
+
|
| 47 |
+
return log_dir
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def create_log_uuid_if_not_exists(log_dir: Path) -> str:
|
| 51 |
+
"""Create a log id file for the current logging session."""
|
| 52 |
+
log_id = get_log_id(log_dir)
|
| 53 |
+
if not log_id.is_file():
|
| 54 |
+
logging_id = f"{uuid.uuid4()}"
|
| 55 |
+
log_id.write_text(logging_id, encoding="utf-8")
|
| 56 |
+
else:
|
| 57 |
+
logging_id = log_id.read_text(encoding="utf-8").rstrip()
|
| 58 |
+
|
| 59 |
+
return logging_id
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_log_id(log_dir):
|
| 63 |
+
"""Get the log id file."""
|
| 64 |
+
return (log_dir / ".logid").absolute()
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def create_uuid_dir_if_not_exists(log_dir, logging_id) -> PosixPath:
|
| 68 |
+
"""Create a directory for the current logging session."""
|
| 69 |
+
uuid_log_dir = (log_dir / logging_id).absolute()
|
| 70 |
+
|
| 71 |
+
if not uuid_log_dir.is_dir():
|
| 72 |
+
uuid_log_dir.mkdir(parents=True, exist_ok=True)
|
| 73 |
+
|
| 74 |
+
return uuid_log_dir
|
openbb_platform/core/openbb_core/app/model/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core App Model."""
|
openbb_platform/core/openbb_core/app/model/abstract/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core App Abstract Model."""
|
openbb_platform/core/openbb_core/app/model/abstract/error.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Error."""
|
| 2 |
+
|
| 3 |
+
from typing import Optional, Union
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class OpenBBError(Exception):
|
| 7 |
+
"""OpenBB Error."""
|
| 8 |
+
|
| 9 |
+
def __init__(self, original: Optional[Union[str, Exception]] = None):
|
| 10 |
+
"""Initialize the OpenBBError."""
|
| 11 |
+
self.original = original
|
| 12 |
+
super().__init__(str(original))
|
openbb_platform/core/openbb_core/app/model/abstract/results.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core App Model Abstract Results."""
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
|
| 5 |
+
Results = BaseModel
|
openbb_platform/core/openbb_core/app/model/abstract/singleton.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Singleton metaclass implementation."""
|
| 2 |
+
|
| 3 |
+
from typing import Dict, Generic, TypeVar
|
| 4 |
+
|
| 5 |
+
T = TypeVar("T")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class SingletonMeta(type, Generic[T]):
|
| 9 |
+
"""Singleton metaclass."""
|
| 10 |
+
|
| 11 |
+
# TODO : check if we want to update this to be thread safe
|
| 12 |
+
_instances: Dict[T, T] = {}
|
| 13 |
+
|
| 14 |
+
def __call__(cls: "SingletonMeta", *args, **kwargs):
|
| 15 |
+
"""Singleton pattern implementation."""
|
| 16 |
+
if cls not in cls._instances:
|
| 17 |
+
instance = super().__call__(*args, **kwargs)
|
| 18 |
+
cls._instances[cls] = instance
|
| 19 |
+
|
| 20 |
+
return cls._instances[cls]
|
openbb_platform/core/openbb_core/app/model/abstract/tagged.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core App Abstract Model Tagged."""
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
from uuid_extensions import uuid7str # type: ignore
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Tagged(BaseModel):
|
| 8 |
+
"""Model for Tagged."""
|
| 9 |
+
|
| 10 |
+
id: str = Field(default_factory=uuid7str, alias="_id")
|
openbb_platform/core/openbb_core/app/model/abstract/warning.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for warnings."""
|
| 2 |
+
|
| 3 |
+
from warnings import WarningMessage
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Warning_(BaseModel):
|
| 9 |
+
"""Model for Warning."""
|
| 10 |
+
|
| 11 |
+
category: str
|
| 12 |
+
message: str
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def cast_warning(w: WarningMessage) -> Warning_:
|
| 16 |
+
"""Cast a warning to a pydantic model."""
|
| 17 |
+
return Warning_(
|
| 18 |
+
category=w.category.__name__,
|
| 19 |
+
message=str(w.message),
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class OpenBBWarning(Warning):
|
| 24 |
+
"""Base class for OpenBB warnings."""
|
openbb_platform/core/openbb_core/app/model/api_settings.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI configuration settings model."""
|
| 2 |
+
|
| 3 |
+
from typing import Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Cors(BaseModel):
|
| 9 |
+
"""Cors model for FastAPI configuration."""
|
| 10 |
+
|
| 11 |
+
model_config = ConfigDict(frozen=True)
|
| 12 |
+
|
| 13 |
+
allow_origins: List[str] = Field(default_factory=lambda: ["*"])
|
| 14 |
+
allow_methods: List[str] = Field(default_factory=lambda: ["*"])
|
| 15 |
+
allow_headers: List[str] = Field(default_factory=lambda: ["*"])
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Servers(BaseModel):
|
| 19 |
+
"""Servers model for FastAPI configuration."""
|
| 20 |
+
|
| 21 |
+
model_config = ConfigDict(frozen=True)
|
| 22 |
+
|
| 23 |
+
url: str = ""
|
| 24 |
+
description: str = "Local OpenBB development server"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class APISettings(BaseModel):
|
| 28 |
+
"""Settings model for FastAPI configuration."""
|
| 29 |
+
|
| 30 |
+
model_config = ConfigDict(frozen=True)
|
| 31 |
+
|
| 32 |
+
version: str = "1"
|
| 33 |
+
title: str = "OpenBB Platform API"
|
| 34 |
+
description: str = "Investment research for everyone, anywhere."
|
| 35 |
+
terms_of_service: str = "http://example.com/terms/"
|
| 36 |
+
contact_name: str = "OpenBB Team"
|
| 37 |
+
contact_url: str = "https://openbb.co"
|
| 38 |
+
contact_email: str = "hello@openbb.co"
|
| 39 |
+
license_name: str = "AGPLv3"
|
| 40 |
+
license_url: str = "https://github.com/OpenBB-finance/OpenBB/blob/develop/LICENSE"
|
| 41 |
+
servers: List[Servers] = Field(default_factory=lambda: [Servers()])
|
| 42 |
+
cors: Cors = Field(default_factory=Cors)
|
| 43 |
+
custom_headers: Optional[Dict[str, str]] = Field(
|
| 44 |
+
default=None, description="Custom headers and respective default value."
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
@computed_field # type: ignore[misc]
|
| 48 |
+
@property
|
| 49 |
+
def prefix(self) -> str:
|
| 50 |
+
"""Return the API prefix."""
|
| 51 |
+
return f"/api/v{self.version}"
|
| 52 |
+
|
| 53 |
+
def __repr__(self) -> str:
|
| 54 |
+
"""Return a string representation of the model."""
|
| 55 |
+
return f"{self.__class__.__name__}\n\n" + "\n".join(
|
| 56 |
+
f"{k}: {v}" for k, v in self.model_dump().items()
|
| 57 |
+
)
|
openbb_platform/core/openbb_core/app/model/charts/chart.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB Core Chart model."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, ConfigDict, Field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Chart(BaseModel):
|
| 9 |
+
"""Model for Chart."""
|
| 10 |
+
|
| 11 |
+
content: Optional[Dict[str, Any]] = Field(
|
| 12 |
+
default=None,
|
| 13 |
+
description="Raw textual representation of the chart.",
|
| 14 |
+
)
|
| 15 |
+
format: Optional[str] = Field(
|
| 16 |
+
default=None,
|
| 17 |
+
description="Complementary attribute to the `content` attribute. It specifies the format of the chart.",
|
| 18 |
+
)
|
| 19 |
+
fig: Optional[Any] = Field(
|
| 20 |
+
default=None,
|
| 21 |
+
description="The figure object.",
|
| 22 |
+
json_schema_extra={"exclude_from_api": True},
|
| 23 |
+
)
|
| 24 |
+
model_config = ConfigDict(validate_assignment=True)
|
| 25 |
+
|
| 26 |
+
def __repr__(self) -> str:
|
| 27 |
+
"""Return string representation."""
|
| 28 |
+
return f"{self.__class__.__name__}\n\n" + "\n".join(
|
| 29 |
+
f"{k}: {v}" for k, v in self.model_dump().items()
|
| 30 |
+
)
|
openbb_platform/core/openbb_core/app/model/charts/charting_settings.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Charting settings."""
|
| 2 |
+
|
| 3 |
+
import importlib
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import TYPE_CHECKING, Optional
|
| 6 |
+
|
| 7 |
+
from openbb_core.app.logs.utils.utils import get_app_id
|
| 8 |
+
from openbb_core.env import Env
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from openbb_core.app.model.system_settings import SystemSettings
|
| 12 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# pylint: disable=too-many-instance-attributes
|
| 16 |
+
class ChartingSettings:
|
| 17 |
+
"""Charting settings."""
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
user_settings: Optional["UserSettings"] = None,
|
| 22 |
+
system_settings: Optional["SystemSettings"] = None,
|
| 23 |
+
):
|
| 24 |
+
"""Initialize charting settings."""
|
| 25 |
+
user_settings_module = importlib.import_module(
|
| 26 |
+
"openbb_core.app.model.user_settings", "UserSettings"
|
| 27 |
+
)
|
| 28 |
+
system_settings_module = importlib.import_module(
|
| 29 |
+
"openbb_core.app.model.system_settings", "SystemSettings"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
UserSettings = user_settings_module.UserSettings
|
| 33 |
+
SystemSettings = system_settings_module.SystemSettings
|
| 34 |
+
user_settings = user_settings or UserSettings()
|
| 35 |
+
system_settings = system_settings or SystemSettings()
|
| 36 |
+
|
| 37 |
+
user_data_directory = (
|
| 38 |
+
str(Path.home() / "OpenBBUserData")
|
| 39 |
+
if not user_settings.preferences
|
| 40 |
+
else user_settings.preferences.data_directory
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# System
|
| 44 |
+
self.log_collect: bool = system_settings.log_collect
|
| 45 |
+
self.version: str = system_settings.version
|
| 46 |
+
self.python_version: str = system_settings.python_version
|
| 47 |
+
self.test_mode = system_settings.test_mode
|
| 48 |
+
self.app_id: str = get_app_id(user_data_directory)
|
| 49 |
+
self.debug_mode: bool = system_settings.debug_mode or Env().DEBUG_MODE
|
| 50 |
+
self.headless: bool = system_settings.headless
|
| 51 |
+
# User
|
| 52 |
+
self.user_email: Optional[str] = getattr(
|
| 53 |
+
user_settings.profile.hub_session, "email", None
|
| 54 |
+
)
|
| 55 |
+
self.user_uuid: Optional[str] = getattr(
|
| 56 |
+
user_settings.profile.hub_session, "user_uuid", None
|
| 57 |
+
)
|
| 58 |
+
self.user_exports_directory = user_settings.preferences.export_directory
|
| 59 |
+
self.user_styles_directory = user_settings.preferences.user_styles_directory
|
| 60 |
+
# Theme
|
| 61 |
+
self.chart_style: str = user_settings.preferences.chart_style
|
| 62 |
+
self.table_style = user_settings.preferences.table_style
|
openbb_platform/core/openbb_core/app/model/command_context.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command Context."""
|
| 2 |
+
|
| 3 |
+
from openbb_core.app.model.system_settings import SystemSettings
|
| 4 |
+
from openbb_core.app.model.user_settings import UserSettings
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class CommandContext(BaseModel):
|
| 9 |
+
"""Command Context."""
|
| 10 |
+
|
| 11 |
+
user_settings: UserSettings = Field(default_factory=UserSettings)
|
| 12 |
+
system_settings: SystemSettings = Field(default_factory=SystemSettings)
|
openbb_platform/core/openbb_core/app/model/credentials.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Credentials model and its utilities."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import traceback
|
| 5 |
+
import warnings
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Dict, List, Optional, Tuple
|
| 8 |
+
|
| 9 |
+
from openbb_core.app.constants import USER_SETTINGS_PATH
|
| 10 |
+
from openbb_core.app.extension_loader import ExtensionLoader
|
| 11 |
+
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
| 12 |
+
from openbb_core.app.provider_interface import ProviderInterface
|
| 13 |
+
from openbb_core.env import Env
|
| 14 |
+
from pydantic import (
|
| 15 |
+
BaseModel,
|
| 16 |
+
ConfigDict,
|
| 17 |
+
Field,
|
| 18 |
+
SecretStr,
|
| 19 |
+
create_model,
|
| 20 |
+
)
|
| 21 |
+
from pydantic.functional_serializers import PlainSerializer
|
| 22 |
+
from typing_extensions import Annotated
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class LoadingError(Exception):
|
| 26 |
+
"""Error loading extension."""
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# @model_serializer blocks model_dump with pydantic parameters (include, exclude)
|
| 30 |
+
OBBSecretStr = Annotated[
|
| 31 |
+
SecretStr,
|
| 32 |
+
PlainSerializer(
|
| 33 |
+
lambda x: x.get_secret_value(), return_type=str, when_used="json-unless-none"
|
| 34 |
+
),
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class CredentialsLoader:
|
| 39 |
+
"""Here we create the Credentials model."""
|
| 40 |
+
|
| 41 |
+
credentials: Dict[str, List[str]] = {}
|
| 42 |
+
|
| 43 |
+
def format_credentials(self, additional: dict) -> Dict[str, Tuple[object, None]]:
|
| 44 |
+
"""Prepare credentials map to be used in the Credentials model."""
|
| 45 |
+
formatted: Dict[str, Tuple[object, None]] = {}
|
| 46 |
+
for c_origin, c_list in self.credentials.items():
|
| 47 |
+
for c_name in c_list:
|
| 48 |
+
if c_name in formatted:
|
| 49 |
+
warnings.warn(
|
| 50 |
+
message=f"Skipping '{c_name}', credential already in use.",
|
| 51 |
+
category=OpenBBWarning,
|
| 52 |
+
)
|
| 53 |
+
continue
|
| 54 |
+
formatted[c_name] = (
|
| 55 |
+
Optional[OBBSecretStr],
|
| 56 |
+
Field(default=None, description=c_origin, alias=c_name.upper()),
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
if additional:
|
| 60 |
+
for key, value in additional.items():
|
| 61 |
+
if key in formatted:
|
| 62 |
+
continue
|
| 63 |
+
formatted[key] = (
|
| 64 |
+
Optional[OBBSecretStr],
|
| 65 |
+
Field(default=value, description=key, alias=key.upper()),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
return dict(sorted(formatted.items()))
|
| 69 |
+
|
| 70 |
+
def from_obbject(self) -> None:
|
| 71 |
+
"""Load credentials from OBBject extensions."""
|
| 72 |
+
for ext_name, ext in ExtensionLoader().obbject_objects.items(): # type: ignore[attr-defined]
|
| 73 |
+
try:
|
| 74 |
+
if ext_name in self.credentials:
|
| 75 |
+
warnings.warn(
|
| 76 |
+
message=f"Skipping '{ext_name}', name already in user.",
|
| 77 |
+
category=OpenBBWarning,
|
| 78 |
+
)
|
| 79 |
+
continue
|
| 80 |
+
self.credentials[ext_name] = ext.credentials
|
| 81 |
+
except Exception as e:
|
| 82 |
+
msg = f"Error loading extension: {ext_name}\n"
|
| 83 |
+
if Env().DEBUG_MODE:
|
| 84 |
+
traceback.print_exception(type(e), e, e.__traceback__)
|
| 85 |
+
raise LoadingError(msg + f"\033[91m{e}\033[0m") from e
|
| 86 |
+
warnings.warn(
|
| 87 |
+
message=msg,
|
| 88 |
+
category=OpenBBWarning,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
def from_providers(self) -> None:
|
| 92 |
+
"""Load credentials from providers."""
|
| 93 |
+
self.credentials = ProviderInterface().credentials
|
| 94 |
+
|
| 95 |
+
def load(self) -> BaseModel:
|
| 96 |
+
"""Load credentials from providers."""
|
| 97 |
+
# We load providers first to give them priority choosing credential names
|
| 98 |
+
self.from_providers()
|
| 99 |
+
self.from_obbject()
|
| 100 |
+
path = Path(USER_SETTINGS_PATH)
|
| 101 |
+
additional: dict = {}
|
| 102 |
+
|
| 103 |
+
if path.exists():
|
| 104 |
+
with open(USER_SETTINGS_PATH, encoding="utf-8") as f:
|
| 105 |
+
data = json.load(f)
|
| 106 |
+
if "credentials" in data:
|
| 107 |
+
additional = data["credentials"]
|
| 108 |
+
|
| 109 |
+
model = create_model(
|
| 110 |
+
"Credentials",
|
| 111 |
+
__config__=ConfigDict(validate_assignment=True, populate_by_name=True),
|
| 112 |
+
**self.format_credentials(additional), # type: ignore
|
| 113 |
+
)
|
| 114 |
+
model.origins = self.credentials
|
| 115 |
+
return model
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
_Credentials = CredentialsLoader().load()
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class Credentials(_Credentials): # type: ignore
|
| 122 |
+
"""Credentials model used to store provider credentials."""
|
| 123 |
+
|
| 124 |
+
model_config = ConfigDict(extra="allow")
|
| 125 |
+
|
| 126 |
+
def __repr__(self) -> str:
|
| 127 |
+
"""Define the string representation of the credentials."""
|
| 128 |
+
return (
|
| 129 |
+
self.__class__.__name__
|
| 130 |
+
+ "\n\n"
|
| 131 |
+
+ "\n".join([f"{k}: {v}" for k, v in sorted(self.__dict__.items())])
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
def show(self):
|
| 135 |
+
"""Unmask credentials and print them."""
|
| 136 |
+
print( # noqa: T201
|
| 137 |
+
self.__class__.__name__
|
| 138 |
+
+ "\n\n"
|
| 139 |
+
+ "\n".join(
|
| 140 |
+
[f"{k}: {v}" for k, v in sorted(self.model_dump(mode="json").items())]
|
| 141 |
+
)
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
def update(self, incoming: "Credentials"):
|
| 145 |
+
"""Update current credentials."""
|
| 146 |
+
self.__dict__.update(incoming.model_dump(exclude_none=True))
|
openbb_platform/core/openbb_core/app/model/defaults.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Defaults model."""
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
from warnings import warn
|
| 5 |
+
|
| 6 |
+
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
| 7 |
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Defaults(BaseModel):
|
| 11 |
+
"""Defaults."""
|
| 12 |
+
|
| 13 |
+
model_config = ConfigDict(validate_assignment=True, populate_by_name=True)
|
| 14 |
+
|
| 15 |
+
commands: dict[str, dict[str, Any]] = Field(
|
| 16 |
+
default_factory=dict,
|
| 17 |
+
alias="routes",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
def __repr__(self) -> str:
|
| 21 |
+
"""Return string representation."""
|
| 22 |
+
return f"{self.__class__.__name__}\n\n" + "\n".join(
|
| 23 |
+
f"{k}: {v}" for k, v in self.model_dump().items()
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
@model_validator(mode="before")
|
| 27 |
+
@classmethod
|
| 28 |
+
def validate_before(cls, values: dict) -> dict:
|
| 29 |
+
"""Validate model (before)."""
|
| 30 |
+
key = "commands"
|
| 31 |
+
if "routes" in values:
|
| 32 |
+
if not values.get("routes"):
|
| 33 |
+
del values["routes"]
|
| 34 |
+
show_warnings = values.get("preferences", {}).get("show_warnings")
|
| 35 |
+
if show_warnings is False or show_warnings in ["False", "false"]:
|
| 36 |
+
warn(
|
| 37 |
+
message="The 'routes' key is deprecated within 'defaults' of 'user_settings.json'."
|
| 38 |
+
+ " Suppress this warning by updating the key to 'commands'.",
|
| 39 |
+
category=OpenBBWarning,
|
| 40 |
+
)
|
| 41 |
+
key = "routes"
|
| 42 |
+
|
| 43 |
+
new_values: dict = {"commands": {}}
|
| 44 |
+
for k, v in values.get(key, {}).items():
|
| 45 |
+
clean_k = k.strip("/").replace("/", ".")
|
| 46 |
+
provider = v.get("provider") if v else None
|
| 47 |
+
if isinstance(provider, str):
|
| 48 |
+
v["provider"] = [provider]
|
| 49 |
+
new_values["commands"][clean_k] = v
|
| 50 |
+
|
| 51 |
+
return new_values
|
| 52 |
+
|
| 53 |
+
def update(self, incoming: "Defaults"):
|
| 54 |
+
"""Update current defaults."""
|
| 55 |
+
incoming_commands = incoming.model_dump(exclude_none=True).get("commands", {})
|
| 56 |
+
self.__dict__["commands"].update(incoming_commands)
|
openbb_platform/core/openbb_core/app/model/example.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example class to represent endpoint examples."""
|
| 2 |
+
|
| 3 |
+
from abc import abstractmethod
|
| 4 |
+
from datetime import date, datetime, timedelta
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional, Union, _GenericAlias # type: ignore
|
| 6 |
+
|
| 7 |
+
from pydantic import (
|
| 8 |
+
BaseModel,
|
| 9 |
+
ConfigDict,
|
| 10 |
+
Field,
|
| 11 |
+
computed_field,
|
| 12 |
+
model_validator,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
QUOTE_TYPES = {str, date}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Example(BaseModel):
|
| 19 |
+
"""Example model."""
|
| 20 |
+
|
| 21 |
+
scope: str
|
| 22 |
+
|
| 23 |
+
model_config = ConfigDict(validate_assignment=True)
|
| 24 |
+
|
| 25 |
+
@abstractmethod
|
| 26 |
+
def to_python(self, **kwargs) -> str:
|
| 27 |
+
"""Return a Python code representation of the example."""
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class APIEx(Example):
|
| 31 |
+
"""API Example model."""
|
| 32 |
+
|
| 33 |
+
scope: Literal["api"] = "api"
|
| 34 |
+
description: Optional[str] = Field(
|
| 35 |
+
default=None, description="Optional description unless more than 3 parameters"
|
| 36 |
+
)
|
| 37 |
+
parameters: Dict[str, Union[str, int, float, bool, List[str], List[Dict[str, Any]]]]
|
| 38 |
+
|
| 39 |
+
@computed_field # type: ignore[misc]
|
| 40 |
+
@property
|
| 41 |
+
def provider(self) -> Optional[str]:
|
| 42 |
+
"""Return the provider from the parameters."""
|
| 43 |
+
return self.parameters.get("provider") # type: ignore
|
| 44 |
+
|
| 45 |
+
@model_validator(mode="before")
|
| 46 |
+
@classmethod
|
| 47 |
+
def validate_model(cls, values: dict) -> dict:
|
| 48 |
+
"""Validate model."""
|
| 49 |
+
parameters = values.get("parameters", {})
|
| 50 |
+
if "provider" not in parameters and "data" not in parameters:
|
| 51 |
+
raise ValueError("API example must specify a provider.")
|
| 52 |
+
|
| 53 |
+
provider = parameters.get("provider")
|
| 54 |
+
if provider and not isinstance(provider, str):
|
| 55 |
+
raise ValueError("Provider must be a string.")
|
| 56 |
+
|
| 57 |
+
return values
|
| 58 |
+
|
| 59 |
+
@staticmethod
|
| 60 |
+
def _unpack_type(type_: type) -> set:
|
| 61 |
+
"""Unpack types from types, example Union[List[str], int] -> {typing._GenericAlias, int}."""
|
| 62 |
+
if (
|
| 63 |
+
hasattr(type_, "__args__")
|
| 64 |
+
and type(type_) # pylint: disable=unidiomatic-typecheck
|
| 65 |
+
is not _GenericAlias
|
| 66 |
+
):
|
| 67 |
+
return set().union(*map(APIEx._unpack_type, type_.__args__)) # type: ignore
|
| 68 |
+
return {type_} if isinstance(type_, type) else {type(type_)}
|
| 69 |
+
|
| 70 |
+
@staticmethod
|
| 71 |
+
def _shift(i: int) -> float:
|
| 72 |
+
"""Return a transformation of the integer."""
|
| 73 |
+
return 2 * (i + 1) / (2 * i) % 1 + 1
|
| 74 |
+
|
| 75 |
+
@staticmethod
|
| 76 |
+
def mock_data(
|
| 77 |
+
dataset: Literal["timeseries", "panel"],
|
| 78 |
+
size: int = 5,
|
| 79 |
+
sample: Optional[Dict[str, Any]] = None,
|
| 80 |
+
multiindex: Optional[Dict[str, Any]] = None,
|
| 81 |
+
) -> List[Dict]:
|
| 82 |
+
"""Generate mock data from a sample.
|
| 83 |
+
|
| 84 |
+
Parameters
|
| 85 |
+
----------
|
| 86 |
+
dataset : str
|
| 87 |
+
The type of data to return:
|
| 88 |
+
- 'timeseries': Time series data
|
| 89 |
+
- 'panel': Panel data (multiindex)
|
| 90 |
+
|
| 91 |
+
size : int
|
| 92 |
+
The size of the data to return, default is 5.
|
| 93 |
+
sample : Optional[Dict[str, Any]], optional
|
| 94 |
+
A sample of the data to return, by default None.
|
| 95 |
+
multiindex_names : Optional[List[str]], optional
|
| 96 |
+
The names of the multiindex, by default None.
|
| 97 |
+
|
| 98 |
+
Timeseries default sample:
|
| 99 |
+
{
|
| 100 |
+
"date": "2023-01-01",
|
| 101 |
+
"open": 110.0,
|
| 102 |
+
"high": 120.0,
|
| 103 |
+
"low": 100.0,
|
| 104 |
+
"close": 115.0,
|
| 105 |
+
"volume": 10000,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
Panel default sample:
|
| 109 |
+
{
|
| 110 |
+
"portfolio_value": 100000,
|
| 111 |
+
"risk_free_rate": 0.02,
|
| 112 |
+
}
|
| 113 |
+
multiindex: {"asset_manager": "AM", "time": 0}
|
| 114 |
+
|
| 115 |
+
Returns
|
| 116 |
+
-------
|
| 117 |
+
List[Dict]
|
| 118 |
+
A list of dictionaries with the mock data.
|
| 119 |
+
"""
|
| 120 |
+
if dataset == "timeseries":
|
| 121 |
+
sample = sample or {
|
| 122 |
+
"date": "2023-01-01",
|
| 123 |
+
"open": 110.0,
|
| 124 |
+
"high": 120.0,
|
| 125 |
+
"low": 100.0,
|
| 126 |
+
"close": 115.0,
|
| 127 |
+
"volume": 10000,
|
| 128 |
+
}
|
| 129 |
+
result = []
|
| 130 |
+
for i in range(1, size + 1):
|
| 131 |
+
s = APIEx._shift(i)
|
| 132 |
+
obs = {}
|
| 133 |
+
for k, v in sample.items():
|
| 134 |
+
if k == "date":
|
| 135 |
+
obs[k] = (
|
| 136 |
+
datetime.strptime(v, "%Y-%m-%d") + timedelta(days=i)
|
| 137 |
+
).strftime("%Y-%m-%d")
|
| 138 |
+
else:
|
| 139 |
+
obs[k] = round(v * s, 2)
|
| 140 |
+
result.append(obs)
|
| 141 |
+
return result
|
| 142 |
+
if dataset == "panel":
|
| 143 |
+
sample = sample or {
|
| 144 |
+
"portfolio_value": 100000.0,
|
| 145 |
+
"risk_free_rate": 0.02,
|
| 146 |
+
}
|
| 147 |
+
multiindex = multiindex or {"asset_manager": "AM", "time": 0}
|
| 148 |
+
multiindex_names = list(multiindex.keys())
|
| 149 |
+
idx_1 = multiindex_names[0]
|
| 150 |
+
idx_2 = multiindex_names[1]
|
| 151 |
+
items_per_idx = 2
|
| 152 |
+
item: Dict[str, Any] = {
|
| 153 |
+
"is_multiindex": True,
|
| 154 |
+
"multiindex_names": str(multiindex_names),
|
| 155 |
+
}
|
| 156 |
+
# Iterate over the number of items to create and add them to the result
|
| 157 |
+
result = []
|
| 158 |
+
for i in range(1, size + 1):
|
| 159 |
+
item[idx_1] = f"{idx_1}_{i}"
|
| 160 |
+
for j in range(items_per_idx):
|
| 161 |
+
item[idx_2] = j
|
| 162 |
+
for k, v in sample.items():
|
| 163 |
+
if isinstance(v, str):
|
| 164 |
+
item[k] = f"{v}_{j}"
|
| 165 |
+
else:
|
| 166 |
+
item[k] = round(v * APIEx._shift(i + j), 2)
|
| 167 |
+
result.append(item.copy())
|
| 168 |
+
return result
|
| 169 |
+
raise ValueError(f"Dataset '{dataset}' not found.")
|
| 170 |
+
|
| 171 |
+
def to_python(self, **kwargs) -> str:
|
| 172 |
+
"""Return a Python code representation of the example."""
|
| 173 |
+
indentation = kwargs.get("indentation", "")
|
| 174 |
+
func_path = kwargs.get("func_path", ".func_router.func_name")
|
| 175 |
+
param_types: Dict[str, type] = kwargs.get("param_types", {})
|
| 176 |
+
prompt = kwargs.get("prompt", "")
|
| 177 |
+
|
| 178 |
+
eg = ""
|
| 179 |
+
if self.description:
|
| 180 |
+
eg += f"{indentation}{prompt}# {self.description}\n"
|
| 181 |
+
|
| 182 |
+
eg += f"{indentation}{prompt}obb{func_path}("
|
| 183 |
+
for k, v in self.parameters.items():
|
| 184 |
+
if k in param_types and (type_ := param_types.get(k)):
|
| 185 |
+
if QUOTE_TYPES.intersection(self._unpack_type(type_)):
|
| 186 |
+
eg += f"{k}='{v}', "
|
| 187 |
+
else:
|
| 188 |
+
eg += f"{k}={v}, "
|
| 189 |
+
else:
|
| 190 |
+
eg += f"{k}={v}, "
|
| 191 |
+
|
| 192 |
+
eg = indentation + eg.strip(", ") + ")\n"
|
| 193 |
+
|
| 194 |
+
return eg
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class PythonEx(Example):
|
| 198 |
+
"""Python Example model."""
|
| 199 |
+
|
| 200 |
+
scope: Literal["python"] = "python"
|
| 201 |
+
description: str
|
| 202 |
+
code: List[str]
|
| 203 |
+
|
| 204 |
+
def to_python(self, **kwargs) -> str:
|
| 205 |
+
"""Return a Python code representation of the example."""
|
| 206 |
+
indentation = kwargs.get("indentation", "")
|
| 207 |
+
prompt = kwargs.get("prompt", "")
|
| 208 |
+
|
| 209 |
+
eg = ""
|
| 210 |
+
if self.description:
|
| 211 |
+
eg += f"{indentation}{prompt}# {self.description}\n"
|
| 212 |
+
|
| 213 |
+
for line in self.code:
|
| 214 |
+
eg += f"{indentation}{prompt}{line}\n"
|
| 215 |
+
|
| 216 |
+
return eg
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def filter_list(
|
| 220 |
+
examples: List[Example],
|
| 221 |
+
providers: List[str],
|
| 222 |
+
) -> List[Example]:
|
| 223 |
+
"""Filter list of examples."""
|
| 224 |
+
return [
|
| 225 |
+
e
|
| 226 |
+
for e in examples
|
| 227 |
+
if (isinstance(e, APIEx) and (not e.provider or e.provider in providers))
|
| 228 |
+
or e.scope != "api"
|
| 229 |
+
]
|
openbb_platform/core/openbb_core/app/model/extension.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extension class for OBBject extensions."""
|
| 2 |
+
|
| 3 |
+
import warnings
|
| 4 |
+
from typing import Callable, List, Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Extension:
|
| 8 |
+
"""
|
| 9 |
+
Serves as OBBject extension entry point and must be created by each extension package.
|
| 10 |
+
|
| 11 |
+
See https://docs.openbb.co/platform/development/developer-guidelines/obbject_extensions.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
def __init__(
|
| 15 |
+
self,
|
| 16 |
+
name: str,
|
| 17 |
+
credentials: Optional[List[str]] = None,
|
| 18 |
+
description: Optional[str] = None,
|
| 19 |
+
) -> None:
|
| 20 |
+
"""Initialize the extension.
|
| 21 |
+
|
| 22 |
+
Parameters
|
| 23 |
+
----------
|
| 24 |
+
name : str
|
| 25 |
+
Name of the extension.
|
| 26 |
+
credentials : Optional[List[str]], optional
|
| 27 |
+
List of required credentials, by default None
|
| 28 |
+
description: Optional[str]
|
| 29 |
+
Extension description.
|
| 30 |
+
"""
|
| 31 |
+
self.name = name
|
| 32 |
+
self.credentials = credentials or []
|
| 33 |
+
self.description = description
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def obbject_accessor(self) -> Callable:
|
| 37 |
+
"""Extend an OBBject, inspired by pandas."""
|
| 38 |
+
# pylint: disable=import-outside-toplevel
|
| 39 |
+
# Avoid circular imports
|
| 40 |
+
|
| 41 |
+
from openbb_core.app.model.obbject import OBBject
|
| 42 |
+
|
| 43 |
+
return self.register_accessor(self.name, OBBject)
|
| 44 |
+
|
| 45 |
+
@staticmethod
|
| 46 |
+
def register_accessor(name, cls) -> Callable:
|
| 47 |
+
"""Register a custom accessor."""
|
| 48 |
+
|
| 49 |
+
def decorator(accessor):
|
| 50 |
+
if hasattr(cls, name):
|
| 51 |
+
warnings.warn(
|
| 52 |
+
f"registration of accessor '{repr(accessor)}' under name "
|
| 53 |
+
f"'{repr(name)}' for type '{repr(cls)}' is overriding a preexisting "
|
| 54 |
+
f"attribute with the same name.",
|
| 55 |
+
UserWarning,
|
| 56 |
+
)
|
| 57 |
+
setattr(cls, name, CachedAccessor(name, accessor))
|
| 58 |
+
# pylint: disable=protected-access
|
| 59 |
+
cls.accessors.add(name)
|
| 60 |
+
return accessor
|
| 61 |
+
|
| 62 |
+
return decorator
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class CachedAccessor:
|
| 66 |
+
"""CachedAccessor."""
|
| 67 |
+
|
| 68 |
+
def __init__(self, name: str, accessor) -> None:
|
| 69 |
+
"""Initialize the cached accessor."""
|
| 70 |
+
self._name = name
|
| 71 |
+
self._accessor = accessor
|
| 72 |
+
|
| 73 |
+
def __get__(self, obj, cls):
|
| 74 |
+
"""Get the cached accessor."""
|
| 75 |
+
if obj is None:
|
| 76 |
+
return self._accessor
|
| 77 |
+
accessor_obj = self._accessor(obj)
|
| 78 |
+
object.__setattr__(obj, self._name, accessor_obj)
|
| 79 |
+
return accessor_obj
|
openbb_platform/core/openbb_core/app/model/field.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Custom field for OpenBB."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, List, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic.fields import FieldInfo
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class OpenBBField(FieldInfo):
|
| 9 |
+
"""Custom field for OpenBB."""
|
| 10 |
+
|
| 11 |
+
def __repr__(self):
|
| 12 |
+
"""Override FieldInfo __repr__."""
|
| 13 |
+
# We use repr() to avoid decoding special characters like \n
|
| 14 |
+
if self.choices:
|
| 15 |
+
return f"OpenBBField(description={repr(self.description)}, choices={repr(self.choices)})"
|
| 16 |
+
return f"OpenBBField(description={repr(self.description)})"
|
| 17 |
+
|
| 18 |
+
def __init__(self, description: str, choices: Optional[List[Any]] = None):
|
| 19 |
+
"""Initialize OpenBBField."""
|
| 20 |
+
json_schema_extra = {"choices": choices} if choices else None
|
| 21 |
+
super().__init__(description=description, json_schema_extra=json_schema_extra) # type: ignore[arg-type]
|
| 22 |
+
|
| 23 |
+
@property
|
| 24 |
+
def choices(self) -> Optional[List[Any]]:
|
| 25 |
+
"""Custom choices."""
|
| 26 |
+
if self.json_schema_extra:
|
| 27 |
+
return self.json_schema_extra.get("choices") # type: ignore[union-attr,return-value]
|
| 28 |
+
return None
|
openbb_platform/core/openbb_core/app/model/hub/hub_session.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model for HubSession."""
|
| 2 |
+
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, SecretStr, field_serializer
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class HubSession(BaseModel):
|
| 9 |
+
"""Model for HubSession."""
|
| 10 |
+
|
| 11 |
+
username: Optional[str] = None
|
| 12 |
+
email: str
|
| 13 |
+
primary_usage: str
|
| 14 |
+
user_uuid: str
|
| 15 |
+
token_type: str
|
| 16 |
+
access_token: SecretStr
|
| 17 |
+
|
| 18 |
+
def __repr__(self) -> str:
|
| 19 |
+
"""Return string representation."""
|
| 20 |
+
return f"{self.__class__.__name__}\n\n" + "\n".join(
|
| 21 |
+
f"{k}: {v}" for k, v in self.model_dump().items()
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
@field_serializer("access_token", when_used="json-unless-none")
|
| 25 |
+
def _dump_secret(self, v):
|
| 26 |
+
"""Dump secret."""
|
| 27 |
+
return v.get_secret_value()
|
openbb_platform/core/openbb_core/app/model/hub/hub_user_settings.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hub user settings model."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class HubUserSettings(BaseModel):
|
| 9 |
+
"""Hub user settings model."""
|
| 10 |
+
|
| 11 |
+
features_settings: Dict[str, Any] = Field(default_factory=dict)
|
| 12 |
+
features_keys: Dict[str, Optional[str]] = Field(default_factory=dict)
|
| 13 |
+
# features_sources: Dict[str, Any]
|
| 14 |
+
# features_terminal_style: Dict[str, Union[str, Dict[str, str]]]
|
| 15 |
+
|
| 16 |
+
model_config = ConfigDict(validate_assignment=True)
|
| 17 |
+
|
| 18 |
+
@field_validator("features_keys", mode="before", check_fields=False)
|
| 19 |
+
@classmethod
|
| 20 |
+
def to_lower(cls, d: dict) -> dict:
|
| 21 |
+
"""Convert dict keys to lowercase."""
|
| 22 |
+
return {k.lower(): v for k, v in d.items()}
|
openbb_platform/core/openbb_core/app/model/metadata.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Metadata model."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Any, Dict, Optional, Sequence, Union
|
| 5 |
+
|
| 6 |
+
from openbb_core.provider.abstract.data import Data
|
| 7 |
+
from pydantic import BaseModel, Field, field_validator
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Metadata(BaseModel):
|
| 11 |
+
"""Metadata of a command execution."""
|
| 12 |
+
|
| 13 |
+
arguments: Dict[str, Any] = Field(
|
| 14 |
+
default_factory=dict,
|
| 15 |
+
description="Arguments of the command.",
|
| 16 |
+
)
|
| 17 |
+
duration: int = Field(
|
| 18 |
+
description="Execution duration in nano second of the command."
|
| 19 |
+
)
|
| 20 |
+
route: str = Field(description="Route of the command.")
|
| 21 |
+
timestamp: datetime = Field(description="Execution starting timestamp.")
|
| 22 |
+
|
| 23 |
+
def __repr__(self) -> str:
|
| 24 |
+
"""Return string representation."""
|
| 25 |
+
return f"{self.__class__.__name__}\n\n" + "\n".join(
|
| 26 |
+
f"{k}: {v}" for k, v in self.model_dump().items()
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
@field_validator("arguments")
|
| 30 |
+
@classmethod
|
| 31 |
+
def scale_arguments(cls, v):
|
| 32 |
+
"""Scale arguments.
|
| 33 |
+
|
| 34 |
+
This function is meant to limit the size of the input arguments of a command.
|
| 35 |
+
If the type is one of the following: `Data`, `List[Data]`, `DataFrame`, `List[DataFrame]`,
|
| 36 |
+
`Series`, `List[Series]` or `ndarray`, the value of the argument is swapped by a dictionary
|
| 37 |
+
containing the type and the columns. If the type is not one of the previous, the
|
| 38 |
+
value is kept or trimmed to 80 characters.
|
| 39 |
+
"""
|
| 40 |
+
# pylint: disable=import-outside-toplevel
|
| 41 |
+
from inspect import isclass # noqa
|
| 42 |
+
from numpy import ndarray # noqa
|
| 43 |
+
from pandas import DataFrame, Series # noqa
|
| 44 |
+
|
| 45 |
+
arguments: Dict[str, Any] = {}
|
| 46 |
+
for item in ["provider_choices", "standard_params", "extra_params"]:
|
| 47 |
+
arguments[item] = {}
|
| 48 |
+
# The item could be class or it could a dictionary.
|
| 49 |
+
v_item = (
|
| 50 |
+
v.__dict__.get(item, {}) if not isinstance(v, dict) else v.get(item, {})
|
| 51 |
+
)
|
| 52 |
+
# The item might not be a dictionary yet.
|
| 53 |
+
v_item = v_item if isinstance(v_item, dict) else v_item.__dict__
|
| 54 |
+
for arg, arg_val in v_item.items():
|
| 55 |
+
new_arg_val: Optional[Union[str, dict[str, Sequence[Any]]]] = None
|
| 56 |
+
|
| 57 |
+
# Data
|
| 58 |
+
if isclass(type(arg_val)) and issubclass(type(arg_val), Data):
|
| 59 |
+
new_arg_val = {
|
| 60 |
+
"type": f"{type(arg_val).__name__}",
|
| 61 |
+
"columns": list(arg_val.model_dump().keys()),
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# List[Data]
|
| 65 |
+
if isinstance(arg_val, list) and issubclass(type(arg_val[0]), Data):
|
| 66 |
+
_columns = [list(d.model_dump().keys()) for d in arg_val]
|
| 67 |
+
ld_columns = (
|
| 68 |
+
item for sublist in _columns for item in sublist
|
| 69 |
+
) # flatten
|
| 70 |
+
new_arg_val = {
|
| 71 |
+
"type": f"List[{type(arg_val[0]).__name__}]",
|
| 72 |
+
"columns": list(set(ld_columns)),
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# DataFrame
|
| 76 |
+
elif isinstance(arg_val, DataFrame):
|
| 77 |
+
df_columns = (
|
| 78 |
+
list(arg_val.index.names) + arg_val.columns.tolist()
|
| 79 |
+
if any(index is not None for index in list(arg_val.index.names))
|
| 80 |
+
else arg_val.columns.tolist()
|
| 81 |
+
)
|
| 82 |
+
new_arg_val = {
|
| 83 |
+
"type": f"{type(arg_val).__name__}",
|
| 84 |
+
"columns": df_columns,
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
# List[DataFrame]
|
| 88 |
+
elif isinstance(arg_val, list) and issubclass(
|
| 89 |
+
type(arg_val[0]), DataFrame
|
| 90 |
+
):
|
| 91 |
+
ldf_columns = [
|
| 92 |
+
(
|
| 93 |
+
list(df.index.names) + df.columns.tolist()
|
| 94 |
+
if any(index is not None for index in list(df.index.names))
|
| 95 |
+
else df.columns.tolist()
|
| 96 |
+
)
|
| 97 |
+
for df in arg_val
|
| 98 |
+
]
|
| 99 |
+
new_arg_val = {
|
| 100 |
+
"type": f"List[{type(arg_val[0]).__name__}]",
|
| 101 |
+
"columns": ldf_columns,
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
# Series
|
| 105 |
+
elif isinstance(arg_val, Series):
|
| 106 |
+
new_arg_val = {
|
| 107 |
+
"type": f"{type(arg_val).__name__}",
|
| 108 |
+
"columns": list(arg_val.index.names) + [arg_val.name],
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
# List[Series]
|
| 112 |
+
elif isinstance(arg_val, list) and isinstance(arg_val[0], Series):
|
| 113 |
+
ls_columns = [
|
| 114 |
+
(
|
| 115 |
+
list(series.index.names) + [series.name]
|
| 116 |
+
if any(
|
| 117 |
+
index is not None for index in list(series.index.names)
|
| 118 |
+
)
|
| 119 |
+
else series.name
|
| 120 |
+
)
|
| 121 |
+
for series in arg_val
|
| 122 |
+
]
|
| 123 |
+
new_arg_val = {
|
| 124 |
+
"type": f"List[{type(arg_val[0]).__name__}]",
|
| 125 |
+
"columns": ls_columns,
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
# ndarray
|
| 129 |
+
elif isinstance(arg_val, ndarray):
|
| 130 |
+
new_arg_val = {
|
| 131 |
+
"type": f"{type(arg_val).__name__}",
|
| 132 |
+
"columns": list(arg_val.dtype.names or []),
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
else:
|
| 136 |
+
str_repr_arg_val = str(arg_val)
|
| 137 |
+
if len(str_repr_arg_val) > 80:
|
| 138 |
+
new_arg_val = str_repr_arg_val[:80]
|
| 139 |
+
|
| 140 |
+
arguments[item][arg] = new_arg_val or arg_val
|
| 141 |
+
|
| 142 |
+
return arguments
|