CatPtain commited on
Commit
3269d97
·
verified ·
1 Parent(s): 7f82fb5

Upload 355 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. openbb_platform/core/README.md +57 -0
  2. openbb_platform/core/__init__.py +1 -0
  3. openbb_platform/core/integration/test_obbject.py +96 -0
  4. openbb_platform/core/openbb_core/__init__.py +1 -0
  5. openbb_platform/core/openbb_core/api/app_loader.py +45 -0
  6. openbb_platform/core/openbb_core/api/auth/user.py +57 -0
  7. openbb_platform/core/openbb_core/api/dependency/__init__.py +1 -0
  8. openbb_platform/core/openbb_core/api/dependency/coverage.py +21 -0
  9. openbb_platform/core/openbb_core/api/dependency/system.py +20 -0
  10. openbb_platform/core/openbb_core/api/exception_handlers.py +134 -0
  11. openbb_platform/core/openbb_core/api/rest_api.py +105 -0
  12. openbb_platform/core/openbb_core/api/router/__init__.py +1 -0
  13. openbb_platform/core/openbb_core/api/router/commands.py +266 -0
  14. openbb_platform/core/openbb_core/api/router/coverage.py +105 -0
  15. openbb_platform/core/openbb_core/api/router/helpers/__init__.py +1 -0
  16. openbb_platform/core/openbb_core/api/router/helpers/coverage_helpers.py +120 -0
  17. openbb_platform/core/openbb_core/api/router/system.py +16 -0
  18. openbb_platform/core/openbb_core/api/router/user.py +18 -0
  19. openbb_platform/core/openbb_core/app/__init__.py +1 -0
  20. openbb_platform/core/openbb_core/app/command_runner.py +512 -0
  21. openbb_platform/core/openbb_core/app/constants.py +8 -0
  22. openbb_platform/core/openbb_core/app/deprecation.py +65 -0
  23. openbb_platform/core/openbb_core/app/extension_loader.py +177 -0
  24. openbb_platform/core/openbb_core/app/logs/formatters/formatter_with_exceptions.py +208 -0
  25. openbb_platform/core/openbb_core/app/logs/handlers/path_tracking_file_handler.py +86 -0
  26. openbb_platform/core/openbb_core/app/logs/handlers/posthog_handler.py +151 -0
  27. openbb_platform/core/openbb_core/app/logs/handlers_manager.py +89 -0
  28. openbb_platform/core/openbb_core/app/logs/logging_service.py +260 -0
  29. openbb_platform/core/openbb_core/app/logs/models/logging_settings.py +58 -0
  30. openbb_platform/core/openbb_core/app/logs/utils/expired_files.py +30 -0
  31. openbb_platform/core/openbb_core/app/logs/utils/utils.py +74 -0
  32. openbb_platform/core/openbb_core/app/model/__init__.py +1 -0
  33. openbb_platform/core/openbb_core/app/model/abstract/__init__.py +1 -0
  34. openbb_platform/core/openbb_core/app/model/abstract/error.py +12 -0
  35. openbb_platform/core/openbb_core/app/model/abstract/results.py +5 -0
  36. openbb_platform/core/openbb_core/app/model/abstract/singleton.py +20 -0
  37. openbb_platform/core/openbb_core/app/model/abstract/tagged.py +10 -0
  38. openbb_platform/core/openbb_core/app/model/abstract/warning.py +24 -0
  39. openbb_platform/core/openbb_core/app/model/api_settings.py +57 -0
  40. openbb_platform/core/openbb_core/app/model/charts/chart.py +30 -0
  41. openbb_platform/core/openbb_core/app/model/charts/charting_settings.py +62 -0
  42. openbb_platform/core/openbb_core/app/model/command_context.py +12 -0
  43. openbb_platform/core/openbb_core/app/model/credentials.py +146 -0
  44. openbb_platform/core/openbb_core/app/model/defaults.py +56 -0
  45. openbb_platform/core/openbb_core/app/model/example.py +229 -0
  46. openbb_platform/core/openbb_core/app/model/extension.py +79 -0
  47. openbb_platform/core/openbb_core/app/model/field.py +28 -0
  48. openbb_platform/core/openbb_core/app/model/hub/hub_session.py +27 -0
  49. openbb_platform/core/openbb_core/app/model/hub/hub_user_settings.py +22 -0
  50. 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