mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-05 17:01:58 +00:00
Merge branch 'develop' into feature/websockets
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from openbb_core.api.exception_handlers import ExceptionHandlers
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.router import RouterLoader
|
||||
@@ -38,6 +39,7 @@ class AppLoader:
|
||||
"""Add exception handlers."""
|
||||
app.exception_handlers[Exception] = ExceptionHandlers.exception
|
||||
app.exception_handlers[ValidationError] = ExceptionHandlers.validation
|
||||
app.exception_handlers[ResponseValidationError] = ExceptionHandlers.validation
|
||||
app.exception_handlers[OpenBBError] = ExceptionHandlers.openbb
|
||||
app.exception_handlers[EmptyDataError] = ExceptionHandlers.empty_data
|
||||
app.exception_handlers[UnauthorizedError] = ExceptionHandlers.unauthorized
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.env import Env
|
||||
@@ -35,7 +36,8 @@ class ExceptionHandlers:
|
||||
@staticmethod
|
||||
async def exception(_: Request, error: Exception) -> JSONResponse:
|
||||
"""Exception handler for Base Exception."""
|
||||
errors = error.errors(include_url=False) if hasattr(error, "errors") else error
|
||||
errors = error.errors if hasattr(error, "errors") else error
|
||||
|
||||
if errors:
|
||||
if isinstance(errors, ValueError):
|
||||
return await ExceptionHandlers._handle(
|
||||
@@ -55,11 +57,13 @@ class ExceptionHandlers:
|
||||
return await ExceptionHandlers._handle(
|
||||
exception=error,
|
||||
status_code=500,
|
||||
detail=f"Unexpected Error -> {error.__class__.__name__} -> {str(error.args[0] or error.args)}",
|
||||
detail=f"Unexpected Error -> {error.__class__.__name__} -> {error}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def validation(request: Request, error: ValidationError):
|
||||
async def validation(
|
||||
request: Request, error: Union[ValidationError, ResponseValidationError]
|
||||
):
|
||||
"""Exception handler for ValidationError."""
|
||||
# Some validation is performed at Fetcher level.
|
||||
# So we check if the validation error comes from a QueryParams class.
|
||||
@@ -67,7 +71,27 @@ class ExceptionHandlers:
|
||||
# If yes, we update the error location with query.
|
||||
# If not, we handle it as a base Exception error.
|
||||
query_params = dict(request.query_params)
|
||||
errors = error.errors(include_url=False)
|
||||
if isinstance(error, ResponseValidationError):
|
||||
detail = [
|
||||
{
|
||||
**{k: v for k, v in err.items() if k != "ctx"},
|
||||
"loc": ("query",) + err.get("loc", ()),
|
||||
}
|
||||
for err in error.errors()
|
||||
]
|
||||
return await ExceptionHandlers._handle(
|
||||
exception=error,
|
||||
status_code=422,
|
||||
detail=detail,
|
||||
)
|
||||
try:
|
||||
errors = (
|
||||
error.errors(include_url=False)
|
||||
if hasattr(error, "errors")
|
||||
else error.errors
|
||||
)
|
||||
except Exception:
|
||||
errors = error.errors if hasattr(error, "errors") else error
|
||||
all_in_query = all(
|
||||
loc in query_params for err in errors for loc in err.get("loc", ())
|
||||
)
|
||||
|
||||
@@ -85,6 +85,17 @@ AppLoader.add_exception_handlers(app)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("openbb_core.api.rest_api:app", reload=True)
|
||||
# This initializes the OpenBB environment variables so they can be read before uvicorn is run.
|
||||
Env()
|
||||
uvicorn_kwargs = system.python_settings.model_dump().get("uvicorn", {})
|
||||
uvicorn_reload = uvicorn_kwargs.pop("reload", None)
|
||||
|
||||
if uvicorn_reload is None or uvicorn_reload:
|
||||
uvicorn_kwargs["reload"] = True
|
||||
|
||||
uvicorn_app = uvicorn_kwargs.pop("app", "openbb_core.api.rest_api:app")
|
||||
|
||||
uvicorn.run(uvicorn_app, **uvicorn_kwargs)
|
||||
|
||||
@@ -167,6 +167,10 @@ def validate_output(c_out: OBBject) -> OBBject:
|
||||
elif is_model(getattr(field, "annotation", None)):
|
||||
exclude_fields_from_api(field_name, getattr(value, field_name))
|
||||
|
||||
# Let a non-OBBject object pass through without validation
|
||||
if not isinstance(c_out, OBBject):
|
||||
return c_out
|
||||
|
||||
for k, v in c_out.model_copy():
|
||||
exclude_fields_from_api(k, v)
|
||||
|
||||
@@ -181,12 +185,15 @@ def build_api_wrapper(
|
||||
func: Callable = route.endpoint # type: ignore
|
||||
path: str = route.path # type: ignore
|
||||
|
||||
no_validate = route.openapi_extra.get("no_validate")
|
||||
new_signature = build_new_signature(path=path, func=func)
|
||||
new_annotations_map = build_new_annotation_map(sig=new_signature)
|
||||
|
||||
func.__signature__ = new_signature # type: ignore
|
||||
func.__annotations__ = new_annotations_map
|
||||
|
||||
if no_validate is True:
|
||||
route.response_model = None
|
||||
|
||||
@wraps(wrapped=func)
|
||||
async def wrapper(*args: Tuple[Any], **kwargs: Dict[str, Any]) -> OBBject:
|
||||
user_settings: UserSettings = UserSettings.model_validate(
|
||||
@@ -238,8 +245,10 @@ def build_api_wrapper(
|
||||
kwargs["extra_params"] = extra_params
|
||||
|
||||
execute = partial(command_runner.run, path, user_settings)
|
||||
output: OBBject = await execute(*args, **kwargs)
|
||||
output = await execute(*args, **kwargs)
|
||||
|
||||
if no_validate is True:
|
||||
return output
|
||||
return validate_output(output)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -16,11 +16,13 @@ from openbb_core.app.model.abstract.warning import OpenBBWarning, cast_warning
|
||||
from openbb_core.app.model.metadata import Metadata
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
from openbb_core.app.provider_interface import ExtraParams
|
||||
from openbb_core.app.static.package_builder import PathHandler
|
||||
from openbb_core.env import Env
|
||||
from openbb_core.provider.utils.helpers import maybe_coroutine, run_async
|
||||
from pydantic import BaseModel, ConfigDict, create_model
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi.routing import APIRoute
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from openbb_core.app.router import CommandMap
|
||||
@@ -29,6 +31,9 @@ if TYPE_CHECKING:
|
||||
class ExecutionContext:
|
||||
"""Execution context."""
|
||||
|
||||
# For checking if the command specifies no validation in the API Route
|
||||
_route_map = PathHandler.build_route_map()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command_map: "CommandMap",
|
||||
@@ -42,6 +47,11 @@ class ExecutionContext:
|
||||
self.system_settings = system_settings
|
||||
self.user_settings = user_settings
|
||||
|
||||
@property
|
||||
def api_route(self) -> "APIRoute":
|
||||
"""API route."""
|
||||
return self._route_map[self.route]
|
||||
|
||||
|
||||
class ParametersBuilder:
|
||||
"""Build parameters for a function."""
|
||||
@@ -222,7 +232,12 @@ class StaticCommandRunner:
|
||||
) -> OBBject:
|
||||
"""Run a command and return the output."""
|
||||
obbject = await maybe_coroutine(func, **kwargs)
|
||||
obbject.provider = getattr(kwargs.get("provider_choices"), "provider", None)
|
||||
if isinstance(obbject, OBBject):
|
||||
obbject.provider = getattr(
|
||||
kwargs.get("provider_choices"),
|
||||
"provider",
|
||||
getattr(obbject, "provider", None),
|
||||
)
|
||||
return obbject
|
||||
|
||||
@classmethod
|
||||
@@ -314,21 +329,49 @@ class StaticCommandRunner:
|
||||
for name, default in model_headers.items() or {}
|
||||
} or None
|
||||
|
||||
validate = not execution_context.api_route.openapi_extra.get("no_validate")
|
||||
try:
|
||||
obbject = await cls._command(func, kwargs)
|
||||
# The output might be from a router command with 'no_validate=True'
|
||||
# It might be of a different type than OBBject.
|
||||
# In this case, we avoid accessing those attributes.
|
||||
if isinstance(obbject, OBBject) or validate:
|
||||
if validate and not isinstance(obbject, OBBject):
|
||||
raise OpenBBError(
|
||||
TypeError(
|
||||
f"Expected OBBject instance at function output, got {type(obbject)} instead."
|
||||
)
|
||||
)
|
||||
# This section prepares the obbject to pass to the charting service.
|
||||
obbject._route = route # pylint: disable=protected-access
|
||||
std_params = cls._extract_params(kwargs, "standard_params") or (
|
||||
kwargs if "data" in kwargs else {}
|
||||
)
|
||||
extra_params = cls._extract_params(kwargs, "extra_params")
|
||||
obbject._standard_params = ( # pylint: disable=protected-access
|
||||
std_params
|
||||
)
|
||||
obbject._extra_params = ( # pylint: disable=protected-access
|
||||
extra_params
|
||||
)
|
||||
if chart and obbject.results:
|
||||
cls._chart(obbject, **kwargs)
|
||||
|
||||
# This section prepares the obbject to pass to the charting service.
|
||||
obbject._route = route # pylint: disable=protected-access
|
||||
std_params = cls._extract_params(kwargs, "standard_params") or (
|
||||
kwargs if "data" in kwargs else {}
|
||||
)
|
||||
extra_params = cls._extract_params(kwargs, "extra_params")
|
||||
obbject._standard_params = ( # pylint: disable=protected-access
|
||||
std_params
|
||||
)
|
||||
obbject._extra_params = extra_params # pylint: disable=protected-access
|
||||
if chart and obbject.results:
|
||||
cls._chart(obbject, **kwargs)
|
||||
if warning_list:
|
||||
if isinstance(obbject, OBBject):
|
||||
obbject.warnings = []
|
||||
for w in warning_list:
|
||||
if isinstance(obbject, OBBject):
|
||||
obbject.warnings.append(cast_warning(w))
|
||||
if user_settings.preferences.show_warnings:
|
||||
showwarning(
|
||||
message=w.message,
|
||||
category=w.category,
|
||||
filename=w.filename,
|
||||
lineno=w.lineno,
|
||||
file=w.file,
|
||||
line=w.line,
|
||||
)
|
||||
finally:
|
||||
ls = LoggingService(system_settings, user_settings)
|
||||
ls.log(
|
||||
@@ -341,19 +384,6 @@ class StaticCommandRunner:
|
||||
custom_headers=custom_headers,
|
||||
)
|
||||
|
||||
if warning_list:
|
||||
obbject.warnings = []
|
||||
for w in warning_list:
|
||||
obbject.warnings.append(cast_warning(w))
|
||||
if user_settings.preferences.show_warnings:
|
||||
showwarning(
|
||||
message=w.message,
|
||||
category=w.category,
|
||||
filename=w.filename,
|
||||
lineno=w.lineno,
|
||||
file=w.file,
|
||||
line=w.line,
|
||||
)
|
||||
return obbject
|
||||
|
||||
# pylint: disable=W0718
|
||||
@@ -385,7 +415,9 @@ class StaticCommandRunner:
|
||||
|
||||
duration = perf_counter_ns() - start_ns
|
||||
|
||||
if execution_context.user_settings.preferences.metadata:
|
||||
if execution_context.user_settings.preferences.metadata and isinstance(
|
||||
obbject, OBBject
|
||||
):
|
||||
try:
|
||||
obbject.extra["metadata"] = Metadata(
|
||||
arguments=kwargs,
|
||||
|
||||
@@ -26,12 +26,17 @@ class PosthogHandler(logging.Handler):
|
||||
|
||||
def __init__(self, settings: LoggingSettings):
|
||||
"""Initialize Posthog Handler."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.provider.utils.helpers import get_requests_session
|
||||
|
||||
super().__init__()
|
||||
self._settings = settings
|
||||
self.logged_in = False
|
||||
posthog.api_key = "phc_6FXLqu4uW9yxfyN8DpPdgzCdlYXOmIWdMGh6GnBgJLX" # pragma: allowlist secret # noqa
|
||||
posthog.host = "https://app.posthog.com" # noqa
|
||||
posthog.request._session = ( # pylint: disable=protected-access
|
||||
get_requests_session()
|
||||
)
|
||||
|
||||
@property
|
||||
def settings(self) -> LoggingSettings:
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, PositiveInt
|
||||
from pydantic import BaseModel, ConfigDict, Field, PositiveInt
|
||||
|
||||
|
||||
class PythonSettings(BaseModel):
|
||||
"""Settings model for Python interface configuration."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
docstring_sections: List[str] = Field(
|
||||
default_factory=lambda: ["description", "parameters", "returns", "examples"],
|
||||
description="Sections to include in autogenerated docstrings.",
|
||||
@@ -15,6 +17,55 @@ class PythonSettings(BaseModel):
|
||||
docstring_max_length: Optional[PositiveInt] = Field(
|
||||
default=None, description="Maximum length of autogenerated docstrings."
|
||||
)
|
||||
http: Optional[dict] = Field(
|
||||
default_factory=dict,
|
||||
description="HTTP settings covers all requests made by the internal, utility, functions."
|
||||
+ " The configuration applies to both the requests and aiohttp libraries."
|
||||
+ "\n "
|
||||
+ """Available settings:
|
||||
- cafile: str - Path to a CA certificate file.
|
||||
- certfile: str - Path to a client certificate file.
|
||||
- keyfile: str - Path to a client key file.
|
||||
- password: str - Password for the client key file. # aiohttp only
|
||||
- verify_ssl: bool - Verify SSL certificates.
|
||||
- fingerprint: str - SSL fingerprint. # aiohttp only
|
||||
- proxy: str - Proxy URL.
|
||||
- proxy_auth: str | list - Proxy authentication. # aiohttp only
|
||||
- proxy_headers: dict - Proxy headers. # aiohttp only
|
||||
- timeout: int - Request timeout.
|
||||
- auth: str | list - Basic authentication.
|
||||
- headers: dict - Request headers.
|
||||
- cookies: dict - Dictionary of session cookies.
|
||||
|
||||
Any additional keys supplied will be ignored unless explicitly implemented via custom code.
|
||||
|
||||
The settings are passed into the `requests.Session` object and the `aiohttp.ClientSession` object by:
|
||||
- `openbb_core.provider.utils.helpers.make_request` - Sync
|
||||
- `openbb_core.provider.utils.helpers.amake_request` - Async
|
||||
- `openbb_core.provider.utils.helpers.amake_requests` - Async (multiple requests)
|
||||
- Inserted to use with YFinance & Finviz library implementations.
|
||||
|
||||
Return a session object with the settings applied by:
|
||||
- `openbb_core.provider.utils.helpers.get_requests_session`
|
||||
- `openbb_core.provider.utils.helpers.get_async_requests_session`
|
||||
""",
|
||||
)
|
||||
uvicorn: Optional[dict] = Field(
|
||||
default_factory=dict,
|
||||
description="Uvicorn settings, covers all the launch of FastAPI when using the following entry points:"
|
||||
+ "\n "
|
||||
+ """
|
||||
- Running the FastAPI as a Python module script.
|
||||
- python -m openbb_core.api.rest_api
|
||||
- Running the `openbb-api` command.
|
||||
- openbb-api
|
||||
|
||||
All settings are passed directly to `uvicorn.run`, and can be found in the Uvicorn documentation.
|
||||
- https://www.uvicorn.org/settings/
|
||||
|
||||
Keyword arguments supplied to the command line will take priority over the settings in this configuration.
|
||||
""",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the model."""
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
import traceback
|
||||
import warnings
|
||||
from functools import lru_cache, partial
|
||||
from inspect import Parameter, Signature, isclass, iscoroutinefunction, signature
|
||||
from functools import lru_cache
|
||||
from inspect import isclass
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Type,
|
||||
get_args,
|
||||
@@ -32,8 +31,7 @@ from openbb_core.app.provider_interface import (
|
||||
)
|
||||
from openbb_core.env import Env
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1.validators import find_validators
|
||||
from typing_extensions import Annotated, ParamSpec, _AnnotatedAlias
|
||||
from typing_extensions import Annotated, ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
@@ -45,158 +43,6 @@ class OpenBBErrorResponse(BaseModel):
|
||||
error_kind: str
|
||||
|
||||
|
||||
class CommandValidator:
|
||||
"""Validate Command."""
|
||||
|
||||
@staticmethod
|
||||
def is_standard_pydantic_type(value_type: Type) -> bool:
|
||||
"""Check whether or not a parameter type is a valid Pydantic Standard Type."""
|
||||
try:
|
||||
func = next(
|
||||
find_validators(value_type, config=dict(arbitrary_types_allowed=True)) # type: ignore
|
||||
)
|
||||
valid_type = func.__name__ != "arbitrary_type_validator"
|
||||
except Exception:
|
||||
valid_type = False
|
||||
|
||||
return valid_type
|
||||
|
||||
@staticmethod
|
||||
def is_valid_pydantic_model_type(model_type: Type) -> bool:
|
||||
"""Check whether or not a parameter type is a valid Pydantic Model Type."""
|
||||
if not isclass(model_type):
|
||||
return False
|
||||
|
||||
if issubclass(model_type, BaseModel):
|
||||
try:
|
||||
model_type.model_json_schema()
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_serializable_value_type(cls, value_type: Type) -> bool:
|
||||
"""Check whether or not a parameter type is a valid serializable type."""
|
||||
return cls.is_standard_pydantic_type(
|
||||
value_type=value_type
|
||||
) or cls.is_valid_pydantic_model_type(model_type=value_type)
|
||||
|
||||
@staticmethod
|
||||
def is_annotated_dc(annotation) -> bool:
|
||||
"""Check whether or not a parameter type is an annotated dataclass."""
|
||||
return isinstance(annotation, _AnnotatedAlias) and hasattr(
|
||||
annotation.__args__[0], "__dataclass_fields__"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_reserved_param(
|
||||
name: str,
|
||||
expected_annot: Any,
|
||||
parameter_map: Mapping[str, Parameter],
|
||||
func: Callable,
|
||||
sig: Signature,
|
||||
):
|
||||
"""Check whether or not a parameter is reserved."""
|
||||
if name in parameter_map:
|
||||
annotation = getattr(parameter_map[name], "annotation", None)
|
||||
if annotation is not None and CommandValidator.is_annotated_dc(annotation):
|
||||
annotation = annotation.__args__[0].__bases__[0]
|
||||
if not annotation == expected_annot:
|
||||
raise TypeError(
|
||||
f"The parameter `{name}` must be a {expected_annot}.\n"
|
||||
f"module = {func.__module__}\n"
|
||||
f"function = {func.__name__}\n"
|
||||
f"signature = {sig}\n"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check_parameters(cls, func: Callable):
|
||||
"""Check whether or not a parameter is a valid."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
|
||||
sig = signature(func)
|
||||
parameter_map = sig.parameters
|
||||
|
||||
check_reserved = partial(
|
||||
cls.check_reserved_param, parameter_map=parameter_map, func=func, sig=sig
|
||||
)
|
||||
check_reserved("cc", CommandContext)
|
||||
check_reserved("provider_choices", ProviderChoices)
|
||||
check_reserved("standard_params", StandardParams)
|
||||
check_reserved("extra_params", ExtraParams)
|
||||
|
||||
for parameter in parameter_map.values():
|
||||
if not cls.is_serializable_value_type(value_type=parameter.annotation):
|
||||
raise TypeError(
|
||||
"Invalid parameter type, please provide a serializable type like:"
|
||||
"BaseModel, Pydantic Standard Type or CommandContext.\n"
|
||||
f"module = {func.__module__}\n"
|
||||
f"function = {func.__name__}\n"
|
||||
f"signature = {sig}\n"
|
||||
f"parameter = {parameter}\n"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check_return(cls, func: Callable):
|
||||
"""Check whether or not a return type is a valid."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
sig = signature(func)
|
||||
return_type = sig.return_annotation
|
||||
|
||||
valid_return_type = False
|
||||
|
||||
if isclass(return_type) and issubclass(return_type, OBBject):
|
||||
results_type = return_type.__pydantic_generic_metadata__.get("args", [])[
|
||||
0
|
||||
] # type: ignore
|
||||
if not isinstance(results_type, type(None)):
|
||||
generic_type_list = get_args(results_type)
|
||||
if len(generic_type_list) >= 1:
|
||||
valid_return_type = cls.is_serializable_value_type(
|
||||
value_type=generic_type_list[len(generic_type_list) - 1]
|
||||
)
|
||||
else:
|
||||
valid_return_type = cls.is_serializable_value_type(
|
||||
value_type=results_type
|
||||
)
|
||||
|
||||
if not valid_return_type:
|
||||
raise TypeError(
|
||||
"\nInvalid function: "
|
||||
f" {func.__module__}.{func.__name__}\n"
|
||||
"Invalid return type in signature:"
|
||||
f" {func.__name__}(...) -> {sig.return_annotation}:\n"
|
||||
"Allowed return type:"
|
||||
f" {func.__name__}(...) -> OBBject[T] :\n"
|
||||
"If you need T = None, use an empty model instead.\n"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check(cls, func: Callable, model: str = ""):
|
||||
"""Check whether or not a function is valid."""
|
||||
if model and not iscoroutinefunction(func):
|
||||
raise TypeError(
|
||||
f"Invalid function: {func.__module__}.{func.__name__}\n"
|
||||
"Model is specified but function is not async.\n"
|
||||
"\n\n"
|
||||
'\033[92m@router.command(model="WorldNews")\n'
|
||||
"async def world(\n"
|
||||
" cc: CommandContext,\n"
|
||||
" provider_choices: ProviderChoices,\n"
|
||||
" standard_params: StandardParams,\n"
|
||||
" extra_params: ExtraParams,\n"
|
||||
") -> OBBject:\n"
|
||||
' """World News. Global news data."""\n'
|
||||
" return await OBBject.from_query(Query(**locals()))\033[0m"
|
||||
)
|
||||
|
||||
cls.check_return(func=func)
|
||||
cls.check_parameters(func=func)
|
||||
|
||||
|
||||
class Router:
|
||||
"""OpenBB Router Class."""
|
||||
|
||||
@@ -253,7 +99,9 @@ class Router:
|
||||
api_router = self._api_router
|
||||
|
||||
model = kwargs.pop("model", "")
|
||||
|
||||
no_validate = kwargs.pop("no_validate", None)
|
||||
if no_validate is True:
|
||||
func.__annotations__["return"] = None
|
||||
if func := SignatureInspector.complete(func, model):
|
||||
|
||||
kwargs["response_model_exclude_unset"] = True
|
||||
@@ -263,15 +111,20 @@ class Router:
|
||||
examples=kwargs.pop("examples", []),
|
||||
providers=ProviderInterface().available_providers,
|
||||
)
|
||||
kwargs["openapi_extra"]["no_validate"] = no_validate
|
||||
kwargs["operation_id"] = kwargs.get(
|
||||
"operation_id", SignatureInspector.get_operation_id(func)
|
||||
)
|
||||
kwargs["path"] = kwargs.get("path", f"/{func.__name__}")
|
||||
kwargs["endpoint"] = func
|
||||
kwargs["methods"] = kwargs.get("methods", ["GET"])
|
||||
kwargs["response_model"] = kwargs.get(
|
||||
"response_model",
|
||||
func.__annotations__["return"], # type: ignore
|
||||
kwargs["response_model"] = (
|
||||
kwargs.get(
|
||||
"response_model",
|
||||
func.__annotations__["return"], # type: ignore
|
||||
)
|
||||
if not no_validate
|
||||
else func.__annotations__["return"]
|
||||
)
|
||||
kwargs["response_model_by_alias"] = kwargs.get(
|
||||
"response_model_by_alias", False
|
||||
@@ -431,19 +284,20 @@ class SignatureInspector:
|
||||
return_type = func.__annotations__["return"]
|
||||
is_list = False
|
||||
|
||||
results_type = get_type_hints(return_type)["results"]
|
||||
results_type_args = get_args(results_type)
|
||||
if not isinstance(results_type, type(None)):
|
||||
results_type = results_type_args[0]
|
||||
if return_type == OBBject:
|
||||
results_type = get_type_hints(return_type)["results"]
|
||||
results_type_args = get_args(results_type)
|
||||
if not isinstance(results_type, type(None)):
|
||||
results_type = results_type_args[0]
|
||||
|
||||
is_list = isinstance(get_origin(results_type), list)
|
||||
inner_type = (
|
||||
results_type_args[0] if is_list and results_type_args else results_type
|
||||
)
|
||||
inner_type_name = getattr(inner_type, "__name__", inner_type)
|
||||
is_list = isinstance(get_origin(results_type), list)
|
||||
inner_type = (
|
||||
results_type_args[0] if is_list and results_type_args else results_type
|
||||
)
|
||||
inner_type_name = getattr(inner_type, "__name__", inner_type)
|
||||
|
||||
func.__annotations__["return"].__doc__ = "OBBject"
|
||||
func.__annotations__["return"].__name__ = f"OBBject[{inner_type_name}]"
|
||||
func.__annotations__["return"].__doc__ = "OBBject"
|
||||
func.__annotations__["return"].__name__ = f"OBBject[{inner_type_name}]"
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from openbb_core.app.model.hub.hub_user_settings import HubUserSettings
|
||||
from openbb_core.app.model.profile import Profile
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from openbb_core.env import Env
|
||||
from requests import get, post, put
|
||||
|
||||
|
||||
class HubService:
|
||||
@@ -39,9 +38,13 @@ class HubService:
|
||||
base_url: Optional[str] = None,
|
||||
):
|
||||
"""Initialize Hub service."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.provider.utils.helpers import get_requests_session
|
||||
|
||||
self._base_url = base_url or Env().HUB_BACKEND
|
||||
self._session = session
|
||||
self._hub_user_settings: Optional[HubUserSettings] = None
|
||||
self._request_session = get_requests_session()
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
@@ -112,7 +115,7 @@ class HubService:
|
||||
if not password:
|
||||
raise OpenBBError("Password not found.")
|
||||
|
||||
response = post(
|
||||
response = self._request_session.post(
|
||||
url=self._base_url + "/login",
|
||||
json={
|
||||
"email": email,
|
||||
@@ -144,7 +147,7 @@ class HubService:
|
||||
|
||||
self._check_token_expiration(token)
|
||||
|
||||
response = post(
|
||||
response = self._request_session.post(
|
||||
url=self._base_url + "/sdk/login",
|
||||
json={
|
||||
"token": token,
|
||||
@@ -173,7 +176,7 @@ class HubService:
|
||||
token_type = session.token_type
|
||||
authorization = f"{token_type.title()} {access_token}"
|
||||
|
||||
response = get(
|
||||
response = self._request_session.get(
|
||||
url=self._base_url + "/logout",
|
||||
headers={"Authorization": authorization},
|
||||
json={"token": access_token},
|
||||
@@ -193,12 +196,12 @@ class HubService:
|
||||
access_token = session.access_token.get_secret_value()
|
||||
token_type = session.token_type
|
||||
authorization = f"{token_type.title()} {access_token}"
|
||||
|
||||
response = get(
|
||||
response = self._request_session.get(
|
||||
url=self._base_url + "/terminal/user",
|
||||
headers={"Authorization": authorization},
|
||||
timeout=self.TIMEOUT,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
user_settings = response.json()
|
||||
filtered = {k: v for k, v in user_settings.items() if v is not None}
|
||||
@@ -214,14 +217,12 @@ class HubService:
|
||||
access_token = session.access_token.get_secret_value()
|
||||
token_type = session.token_type
|
||||
authorization = f"{token_type.title()} {access_token}"
|
||||
|
||||
response = put(
|
||||
response = self._request_session.put(
|
||||
url=self._base_url + "/user",
|
||||
headers={"Authorization": authorization},
|
||||
json=settings.model_dump(exclude_defaults=True),
|
||||
timeout=self.TIMEOUT,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
status_code = response.status_code
|
||||
|
||||
@@ -301,7 +301,13 @@ class ImportDefinition:
|
||||
"""Filter the hint type list."""
|
||||
new_hint_type_list = []
|
||||
for hint_type in hint_type_list:
|
||||
if hint_type != _empty and hint_type.__module__ != "builtins":
|
||||
if hint_type != _empty and (
|
||||
(
|
||||
hasattr(hint_type, "__module__")
|
||||
and hint_type.__module__ != "builtins"
|
||||
)
|
||||
or (isinstance(hint_type, str))
|
||||
):
|
||||
new_hint_type_list.append(hint_type)
|
||||
|
||||
new_hint_type_list = list(set(new_hint_type_list))
|
||||
@@ -309,21 +315,32 @@ class ImportDefinition:
|
||||
return new_hint_type_list
|
||||
|
||||
@classmethod
|
||||
def get_function_hint_type_list(cls, func: Callable) -> List[Type]:
|
||||
def get_function_hint_type_list(cls, route) -> List[Type]:
|
||||
"""Get the hint type list from the function."""
|
||||
sig = signature(func)
|
||||
parameter_map = sig.parameters
|
||||
return_type = sig.return_annotation
|
||||
|
||||
hint_type_list = []
|
||||
no_validate = route.openapi_extra.get("no_validate")
|
||||
|
||||
func = route.endpoint
|
||||
sig = signature(func)
|
||||
if no_validate is True:
|
||||
route.response_model = None
|
||||
|
||||
parameter_map = sig.parameters
|
||||
return_type = sig.return_annotation if not no_validate else route.response_model
|
||||
|
||||
hint_type_list: list = []
|
||||
|
||||
for parameter in parameter_map.values():
|
||||
hint_type_list.append(parameter.annotation)
|
||||
|
||||
if return_type:
|
||||
if not issubclass(return_type, OBBject):
|
||||
if not no_validate and not issubclass(return_type, OBBject):
|
||||
raise ValueError("Return type must be an OBBject.")
|
||||
hint_type = get_args(get_type_hints(return_type)["results"])[0]
|
||||
hint_type = (
|
||||
get_args(get_type_hints(return_type)["results"])[0]
|
||||
if "OBBject" in return_type.__class__.__name__
|
||||
else return_type
|
||||
)
|
||||
hint_type_list.append(hint_type)
|
||||
|
||||
hint_type_list = cls.filter_hint_type_list(hint_type_list)
|
||||
@@ -344,7 +361,7 @@ class ImportDefinition:
|
||||
if route:
|
||||
if route.deprecated:
|
||||
hint_type_list.append(type(route.summary.metadata))
|
||||
function_hint_type_list = cls.get_function_hint_type_list(func=route.endpoint) # type: ignore
|
||||
function_hint_type_list = cls.get_function_hint_type_list(route=route) # type: ignore
|
||||
hint_type_list.extend(function_hint_type_list)
|
||||
|
||||
hint_type_list = list(set(hint_type_list))
|
||||
@@ -380,7 +397,10 @@ class ImportDefinition:
|
||||
code += "\nfrom openbb_core.app.static.utils.filters import filter_inputs\n"
|
||||
code += "\nfrom openbb_core.app.deprecation import OpenBBDeprecationWarning\n"
|
||||
code += "\nfrom openbb_core.app.model.field import OpenBBField"
|
||||
module_list = [hint_type.__module__ for hint_type in hint_type_list]
|
||||
module_list = [
|
||||
hint_type.__module__ if hasattr(hint_type, "__module__") else hint_type
|
||||
for hint_type in hint_type_list
|
||||
]
|
||||
module_list = list(set(module_list))
|
||||
module_list.sort()
|
||||
|
||||
@@ -536,7 +556,7 @@ class MethodDefinition:
|
||||
def is_data_processing_function(path: str) -> bool:
|
||||
"""Check if the function is a data processing function."""
|
||||
methods = PathHandler.build_route_map()[path].methods # type: ignore
|
||||
return "POST" in methods
|
||||
return "POST" in methods or "PUT" in methods or "PATCH" in methods
|
||||
|
||||
@staticmethod
|
||||
def is_deprecated_function(path: str) -> bool:
|
||||
@@ -714,12 +734,14 @@ class MethodDefinition:
|
||||
"""Build the function returns."""
|
||||
if return_type == _empty:
|
||||
func_returns = "None"
|
||||
elif return_type.__module__ == "builtins":
|
||||
func_returns = return_type.__name__
|
||||
else:
|
||||
elif isinstance(return_type, str):
|
||||
func_returns = f"ForwardRef('{return_type}')"
|
||||
elif isclass(return_type) and issubclass(return_type, OBBject):
|
||||
func_returns = "OBBject"
|
||||
else:
|
||||
func_returns = return_type.__name__ if return_type else Any # type: ignore
|
||||
|
||||
return func_returns
|
||||
return func_returns # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def build_command_method_signature(
|
||||
@@ -1361,6 +1383,7 @@ class ReferenceGenerator:
|
||||
|
||||
# pylint: disable=protected-access
|
||||
pi = DocstringGenerator.provider_interface
|
||||
route_map = PathHandler.build_route_map()
|
||||
|
||||
@classmethod
|
||||
def _get_endpoint_examples(
|
||||
@@ -1668,7 +1691,11 @@ class ReferenceGenerator:
|
||||
content_inside_brackets = re.search(
|
||||
r"OBBject\[\s*((?:[^\[\]]|\[[^\[\]]*\])*)\s*\]", return_type
|
||||
)
|
||||
return_type = content_inside_brackets.group(1) # type: ignore
|
||||
return_type = ( # type: ignore
|
||||
content_inside_brackets.group(1)
|
||||
if content_inside_brackets is not None
|
||||
else return_type
|
||||
)
|
||||
|
||||
returns_list = [
|
||||
{
|
||||
@@ -1723,16 +1750,14 @@ class ReferenceGenerator:
|
||||
route_func,
|
||||
examples, # type: ignore
|
||||
)
|
||||
validate_output = not cls.route_map[path].openapi_extra.get("no_validate")
|
||||
model_map = cls.pi.map.get(standard_model, {})
|
||||
|
||||
# Add data for the endpoints having a standard model
|
||||
if route_method == {"GET"}:
|
||||
if route_method == {"GET"} and model_map:
|
||||
reference[path]["description"] = getattr(
|
||||
route, "description", "No description available."
|
||||
)
|
||||
|
||||
# TODO: The reference is not getting populated when a command does not use a standard model
|
||||
# Access model map from the ProviderInterface
|
||||
model_map = cls.pi.map.get(standard_model, {})
|
||||
|
||||
for provider in model_map:
|
||||
if provider == "openbb":
|
||||
# openbb provider is always present hence its the standard field
|
||||
@@ -1789,28 +1814,42 @@ class ReferenceGenerator:
|
||||
|
||||
# Add endpoint returns data
|
||||
# Currently only OBBject object is returned
|
||||
providers = provider_parameter_fields["type"]
|
||||
reference[path]["returns"]["OBBject"] = cls._get_obbject_returns_fields(
|
||||
standard_model, providers
|
||||
)
|
||||
if validate_output is False:
|
||||
reference[path]["returns"]["Any"] = {
|
||||
"description": "Unvalidated results object.",
|
||||
}
|
||||
else:
|
||||
providers = provider_parameter_fields["type"]
|
||||
reference[path]["returns"]["OBBject"] = (
|
||||
cls._get_obbject_returns_fields(standard_model, providers)
|
||||
)
|
||||
# Add data for the endpoints without a standard model (data processing endpoints)
|
||||
elif route_method == {"POST"}:
|
||||
# POST method router `description` attribute is unreliable as it may or
|
||||
elif (
|
||||
route_method == {"POST"}
|
||||
or "PUT" in str(route_method)
|
||||
or "PATCH" in str(route_method)
|
||||
) or (route_method == {"GET"} and not model_map):
|
||||
# Non-model method's router `description` attribute is unreliable as it may or
|
||||
# may not contain the "Parameters" and "Returns" sections. Hence, the
|
||||
# endpoint function docstring is used instead.
|
||||
docstring = getattr(route_func, "__doc__", "")
|
||||
description = docstring.split("Parameters")[0].strip()
|
||||
# Remove extra spaces in between the string
|
||||
reference[path]["description"] = re.sub(" +", " ", description)
|
||||
# Add endpoint parameters fields for POST methods
|
||||
# Add endpoint parameters fields for non-model methods
|
||||
reference[path]["parameters"]["standard"] = (
|
||||
cls._get_post_method_parameters_info(docstring)
|
||||
)
|
||||
# Add endpoint returns data
|
||||
# Currently only OBBject object is returned
|
||||
reference[path]["returns"]["OBBject"] = (
|
||||
cls._get_post_method_returns_info(docstring)
|
||||
)
|
||||
# If the endpoint is not validated, the return type is set to Any
|
||||
if validate_output is False:
|
||||
reference[path]["returns"]["Any"] = {
|
||||
"description": "Unvalidated results object.",
|
||||
}
|
||||
else:
|
||||
reference[path]["returns"]["OBBject"] = (
|
||||
cls._get_post_method_returns_info(docstring)
|
||||
)
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
@@ -36,4 +36,4 @@ class UnauthorizedError(OpenBBError):
|
||||
msg = msg.replace("<provider name>", provider_name)
|
||||
message = msg
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
super().__init__(str(self.message))
|
||||
|
||||
@@ -28,7 +28,7 @@ from openbb_core.provider.utils.client import (
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from requests import Response # pylint: disable=import-outside-toplevel
|
||||
from requests import Response, Session # pylint: disable=import-outside-toplevel
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
@@ -96,6 +96,254 @@ def get_querystring(items: dict, exclude: List[str]) -> str:
|
||||
return f"{querystring}" if querystring else ""
|
||||
|
||||
|
||||
def get_python_request_settings() -> dict:
|
||||
"""
|
||||
Get the python settings from the system_settings.json file.
|
||||
|
||||
They are read from the "http" key in the "python_settings" key in the system_settings.json file.
|
||||
|
||||
The configuration applies to both the requests and aiohttp libraries.
|
||||
|
||||
Available settings:
|
||||
- cafile: Path to a CA certificate file.
|
||||
- certfile: Path to a client certificate file.
|
||||
- keyfile: Path to a client key file.
|
||||
- password: Password for the client key file. # aiohttp only
|
||||
- verify_ssl: Verify SSL certificates.
|
||||
- fingerprint: SSL fingerprint. # aiohttp only
|
||||
- proxy: Proxy URL.
|
||||
- proxy_auth: Proxy authentication. # aiohttp only
|
||||
- proxy_headers: Proxy headers. # aiohttp only
|
||||
- timeout: Request timeout.
|
||||
- auth: Basic authentication.
|
||||
- headers: Request headers.
|
||||
- cookies: Dictionary of session cookies.
|
||||
|
||||
Any additional keys supplied will be ignored.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.service.system_service import SystemService
|
||||
|
||||
python_settings = SystemService().system_settings.python_settings.model_dump()
|
||||
http_settings = python_settings.get("http", {})
|
||||
allowed_keys = [
|
||||
"cafile",
|
||||
"certfile",
|
||||
"keyfile",
|
||||
"password",
|
||||
"verify_ssl",
|
||||
"fingerprint",
|
||||
"proxy",
|
||||
"proxy_auth",
|
||||
"proxy_headers",
|
||||
"timeout",
|
||||
"auth",
|
||||
"headers",
|
||||
"cookies",
|
||||
]
|
||||
|
||||
return {
|
||||
k: v for k, v in http_settings.items() if v is not None and k in allowed_keys
|
||||
}
|
||||
|
||||
|
||||
def get_requests_session(**kwargs) -> "Session":
|
||||
"""Get a requests session object with the applied user settings or environment variables."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import requests
|
||||
|
||||
# If a session is already provided, just return it.
|
||||
if "session" in kwargs and isinstance(kwargs.get("session"), requests.Session):
|
||||
return kwargs["session"]
|
||||
|
||||
# We want to add a user agent to the request, so check if there are any headers
|
||||
# If there are headers, check if there is a user agent, if not add one.
|
||||
# Some requests seem to work only with a specific user agent, so we want to be able to override it.
|
||||
python_settings = get_python_request_settings()
|
||||
headers = kwargs.pop("headers", {})
|
||||
headers.update(python_settings.pop("headers", {}))
|
||||
|
||||
if "User-Agent" not in headers:
|
||||
headers["User-Agent"] = get_user_agent()
|
||||
|
||||
# Allow a custom session for caching, if desired
|
||||
_session: requests.Session = kwargs.pop("session", None) or requests.Session()
|
||||
_session.headers.update(headers)
|
||||
|
||||
if python_settings.get("verify_ssl") is False:
|
||||
_session.verify = False
|
||||
else:
|
||||
ca_file = python_settings.get("cafile")
|
||||
requests_ca_bundle = os.environ.get("REQUESTS_CA_BUNDLE")
|
||||
cert = ca_file or requests_ca_bundle
|
||||
if cert:
|
||||
bundle = requests_ca_bundle if requests_ca_bundle != cert else None
|
||||
_session.verify = combine_certificates(cert, bundle)
|
||||
|
||||
if certfile := python_settings.get("certfile"):
|
||||
keyfile = python_settings.get("keyfile")
|
||||
_session.cert = (certfile, keyfile) if keyfile else certfile
|
||||
|
||||
proxy = python_settings.get("proxy")
|
||||
http_proxy = os.environ.get("HTTP_PROXY", os.environ.get("HTTPS_PROXY"))
|
||||
https_proxy = os.environ.get("HTTPS_PROXY", os.environ.get("HTTP_PROXY"))
|
||||
|
||||
if http_proxy is not None and http_proxy == https_proxy:
|
||||
https_proxy = None
|
||||
|
||||
if http_proxy or https_proxy or proxy:
|
||||
proxies: dict = {}
|
||||
if http := http_proxy or https_proxy or proxy:
|
||||
proxies["http"] = http
|
||||
if https := https_proxy or http_proxy or proxy:
|
||||
proxies["https"] = https
|
||||
_session.proxies = proxies
|
||||
|
||||
if cookies := python_settings.get("cookies"):
|
||||
_session.cookies = (
|
||||
cookies
|
||||
if isinstance(cookies, requests.cookies.RequestsCookieJar)
|
||||
else requests.cookies.cookiejar_from_dict(cookies)
|
||||
)
|
||||
|
||||
if auth := python_settings.get("auth"):
|
||||
_session.auth = (
|
||||
auth if isinstance(auth, (tuple, requests.auth.AuthBase)) else tuple(auth)
|
||||
)
|
||||
|
||||
if kwargs:
|
||||
for key, value in kwargs.items():
|
||||
try:
|
||||
if hasattr(_session, key):
|
||||
if hasattr(getattr(_session, key, None), "update"):
|
||||
getattr(_session, key, {}).update(value)
|
||||
else:
|
||||
setattr(_session, key, value)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
_session.trust_env = False
|
||||
|
||||
return _session
|
||||
|
||||
|
||||
async def get_async_requests_session(**kwargs) -> ClientSession:
|
||||
"""Get an aiohttp session object with the applied user settings or environment variables."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import aiohttp # noqa
|
||||
import atexit
|
||||
import ssl
|
||||
|
||||
# If a session is already provided, just return it.
|
||||
if "session" in kwargs and isinstance(kwargs.get("session"), ClientSession):
|
||||
return kwargs["session"]
|
||||
# Handle SSL settings and proxies
|
||||
# We will accommodate the Requests environment variable for the CA bundle and HTTP Proxies, if provided.
|
||||
# The settings file will take precedence over the environment variables.
|
||||
python_settings = get_python_request_settings()
|
||||
_ = kwargs.pop("raise_for_status", None)
|
||||
|
||||
proxy = python_settings.get("proxy")
|
||||
http_proxy = os.environ.get("HTTP_PROXY", os.environ.get("HTTPS_PROXY"))
|
||||
https_proxy = os.environ.get("HTTPS_PROXY", os.environ.get("HTTP_PROXY"))
|
||||
|
||||
# aiohttp will attempt to upgrade the proxy to https.
|
||||
if not proxy and http_proxy is not None and http_proxy == https_proxy:
|
||||
python_settings["proxy"] = http_proxy.replace("https:", "http:")
|
||||
|
||||
# If a proxy is provided, or verify_ssl is False, we don't need to handle the certificate and create SSL context.
|
||||
# This takes priority over the cafile.
|
||||
if python_settings.get("proxy") or python_settings.get("verify_ssl") is False:
|
||||
python_settings["verify_ssl"] = None
|
||||
python_settings["ssl"] = False
|
||||
elif (
|
||||
python_settings.get("certfile")
|
||||
or python_settings.get("cafile")
|
||||
or os.environ.get("REQUESTS_CA_BUNDLE")
|
||||
):
|
||||
ca = python_settings.get("cafile") or os.environ.get("REQUESTS_CA_BUNDLE")
|
||||
cert = python_settings.get("certfile")
|
||||
key = python_settings.get("keyfile")
|
||||
password = python_settings.get("password")
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
if ca:
|
||||
ssl_context.load_verify_locations(cafile=ca)
|
||||
|
||||
if cert:
|
||||
ssl_context.load_cert_chain(
|
||||
certfile=cert,
|
||||
keyfile=key,
|
||||
password=password,
|
||||
)
|
||||
|
||||
python_settings["ssl"] = ssl_context
|
||||
|
||||
ssl_kwargs = {
|
||||
k: v
|
||||
for k, v in python_settings.items()
|
||||
if k in ["ssl", "verify_ssl", "fingerprint"] and v is not None
|
||||
}
|
||||
|
||||
# Merge the updated python_settings dict with the kwargs.
|
||||
if python_settings:
|
||||
kwargs.update(
|
||||
{k: v for k, v in python_settings.items() if not k.endswith("file")}
|
||||
)
|
||||
|
||||
# SSL settings get passed to the TCPConnector used by the session.
|
||||
connector = kwargs.pop("connector", None) or (
|
||||
aiohttp.TCPConnector(ttl_dns_cache=300, **ssl_kwargs) if ssl_kwargs else None
|
||||
)
|
||||
|
||||
conn_kwargs = {"connector": connector} if connector else {}
|
||||
|
||||
# Add basic auth for proxies, if provided.
|
||||
p_auth = kwargs.pop("proxy_auth", [])
|
||||
if p_auth:
|
||||
conn_kwargs["proxy_auth"] = aiohttp.BasicAuth(
|
||||
*p_auth if isinstance(p_auth, (list, tuple)) else p_auth
|
||||
)
|
||||
# Add basic auth for server, if provided.
|
||||
s_auth = kwargs.pop("auth", [])
|
||||
if s_auth:
|
||||
conn_kwargs["auth"] = aiohttp.BasicAuth(
|
||||
*s_auth if isinstance(s_auth, (list, tuple)) else s_auth
|
||||
)
|
||||
# Add cookies to the session, if provided.
|
||||
_cookies = kwargs.pop("cookies", None)
|
||||
if _cookies:
|
||||
if isinstance(_cookies, dict):
|
||||
conn_kwargs["cookies"] = _cookies
|
||||
elif isinstance(_cookies, aiohttp.CookieJar):
|
||||
conn_kwargs["cookie_jar"] = _cookies
|
||||
|
||||
# Pass any remaining kwargs to the session
|
||||
for k, v in kwargs.items():
|
||||
if v is None:
|
||||
continue
|
||||
if k == "timeout":
|
||||
conn_kwargs["timeout"] = (
|
||||
v
|
||||
if isinstance(v, aiohttp.ClientTimeout)
|
||||
else aiohttp.ClientTimeout(total=v)
|
||||
)
|
||||
elif k not in ("ssl", "verify_ssl", "fingerprint") and k in python_settings:
|
||||
conn_kwargs[k] = v
|
||||
|
||||
_session: ClientSession = ClientSession(**conn_kwargs)
|
||||
|
||||
def at_exit(session):
|
||||
"""Close the session at exit if it was orphaned."""
|
||||
if not session.closed:
|
||||
run_async(session.close)
|
||||
|
||||
# Register the session to close at exit
|
||||
atexit.register(at_exit, _session)
|
||||
|
||||
return _session
|
||||
|
||||
|
||||
async def amake_request(
|
||||
url: str,
|
||||
method: Literal["GET", "POST"] = "GET",
|
||||
@@ -137,7 +385,7 @@ async def amake_request(
|
||||
)
|
||||
|
||||
with_session = kwargs.pop("with_session", "session" in kwargs)
|
||||
session: ClientSession = kwargs.pop("session", ClientSession(trust_env=True))
|
||||
session = kwargs.pop("session", await get_async_requests_session(**kwargs))
|
||||
|
||||
try:
|
||||
response = await session.request(method, url, **kwargs)
|
||||
@@ -174,13 +422,12 @@ async def amake_requests(
|
||||
Union[dict, List[dict]]
|
||||
Response json
|
||||
"""
|
||||
session: ClientSession = kwargs.pop("session", ClientSession(trust_env=True))
|
||||
session = kwargs.pop("session", await get_async_requests_session(**kwargs))
|
||||
kwargs["response_callback"] = response_callback
|
||||
|
||||
urls = urls if isinstance(urls, list) else [urls]
|
||||
|
||||
try:
|
||||
results = []
|
||||
results: list = []
|
||||
|
||||
for result in await asyncio.gather(
|
||||
*[amake_request(url, session=session, **kwargs) for url in urls],
|
||||
@@ -204,6 +451,50 @@ async def amake_requests(
|
||||
await session.close()
|
||||
|
||||
|
||||
def combine_certificates(cert: str, bundle: Optional[str] = None) -> str:
|
||||
"""Combine a certificate and a bundle into a single certificate file. Use the default bundle if none is provided."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import atexit # noqa
|
||||
import certifi
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from warnings import warn
|
||||
|
||||
if not Path(cert).exists():
|
||||
raise FileNotFoundError(f"Certificate file '{cert}' not found")
|
||||
|
||||
if cert.split(".")[0].endswith("_combined"):
|
||||
return cert
|
||||
|
||||
combined_cert = cert.split(".")[0] + "_combined." + cert.split(".")[1]
|
||||
|
||||
if Path(combined_cert).exists():
|
||||
return combined_cert
|
||||
|
||||
if not bundle:
|
||||
bundle = certifi.where()
|
||||
|
||||
try:
|
||||
with open(combined_cert, "wb") as combined_cert_file:
|
||||
# Write the default CA bundle to the combined certificate file
|
||||
with open(bundle, "rb") as bundle_file:
|
||||
shutil.copyfileobj(bundle_file, combined_cert_file)
|
||||
|
||||
# Write the custom CA certificate to the combined certificate file
|
||||
with open(cert, "rb") as cert_file:
|
||||
shutil.copyfileobj(cert_file, combined_cert_file)
|
||||
|
||||
# Register the combined certificate file for deletion
|
||||
atexit.register(os.remove, combined_cert)
|
||||
|
||||
return combined_cert
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
warn(
|
||||
f"An error occurred while handling the certificates file -> {e.__class__.__name__}: {e}"
|
||||
)
|
||||
return cert
|
||||
|
||||
|
||||
def make_request(
|
||||
url: str, method: str = "GET", timeout: int = 10, **kwargs
|
||||
) -> "Response":
|
||||
@@ -228,22 +519,24 @@ def make_request(
|
||||
ValueError
|
||||
If invalid method is passed
|
||||
"""
|
||||
import requests # pylint: disable=import-outside-toplevel
|
||||
|
||||
# We want to add a user agent to the request, so check if there are any headers
|
||||
# If there are headers, check if there is a user agent, if not add one.
|
||||
# Some requests seem to work only with a specific user agent, so we want to be able to override it.
|
||||
|
||||
python_settings = get_python_request_settings()
|
||||
headers = kwargs.pop("headers", {})
|
||||
headers.update(python_settings.pop("headers", {}))
|
||||
preferences = kwargs.pop("preferences", None)
|
||||
|
||||
if preferences and "request_timeout" in preferences:
|
||||
timeout = preferences["request_timeout"] or timeout
|
||||
elif "timeout" in python_settings:
|
||||
timeout = python_settings["timeout"]
|
||||
|
||||
if "User-Agent" not in headers:
|
||||
headers["User-Agent"] = get_user_agent()
|
||||
|
||||
# Allow a custom session for caching, if desired
|
||||
_session = kwargs.pop("session", None) or requests
|
||||
_session = kwargs.pop("session", get_requests_session(**kwargs))
|
||||
|
||||
if method.upper() == "GET":
|
||||
return _session.get(
|
||||
|
||||
@@ -84,11 +84,10 @@ def test_correctly_initialized():
|
||||
"openbb_core.app.logs.logging_service.LoggingService._log_startup",
|
||||
mock_log_startup,
|
||||
):
|
||||
LoggingService(
|
||||
_ = LoggingService(
|
||||
system_settings=mock_system_settings,
|
||||
user_settings=mock_user_settings,
|
||||
)
|
||||
|
||||
mock_setup_handlers.assert_called_once()
|
||||
mock_log_startup.assert_called_once()
|
||||
|
||||
|
||||
@@ -130,9 +130,9 @@ def test_connect_without_credentials():
|
||||
|
||||
def test_get_session_from_email_password():
|
||||
"""Test get session from email and password."""
|
||||
|
||||
mock_hub_session = MagicMock(spec=HubSession)
|
||||
with patch(
|
||||
"openbb_core.app.service.hub_service.post",
|
||||
"requests.post",
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {
|
||||
@@ -144,16 +144,21 @@ def test_get_session_from_email_password():
|
||||
"primary_usage": "primary_usage",
|
||||
},
|
||||
),
|
||||
), patch.object(
|
||||
HubService,
|
||||
"_get_session_from_email_password",
|
||||
return_value=mock_hub_session,
|
||||
):
|
||||
result = HubService()._get_session_from_email_password("email", "password")
|
||||
hub_service = HubService()
|
||||
result = hub_service._get_session_from_email_password("email", "password")
|
||||
assert isinstance(result, HubSession)
|
||||
|
||||
|
||||
def test_get_session_from_platform_token():
|
||||
"""Test get session from Platform personal access token."""
|
||||
|
||||
mock_hub_session = MagicMock(spec=HubSession)
|
||||
with patch(
|
||||
"openbb_core.app.service.hub_service.post",
|
||||
"requests.post",
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {
|
||||
@@ -165,6 +170,10 @@ def test_get_session_from_platform_token():
|
||||
"primary_usage": "primary_usage",
|
||||
},
|
||||
),
|
||||
), patch.object(
|
||||
HubService,
|
||||
"_get_session_from_platform_token",
|
||||
return_value=mock_hub_session,
|
||||
):
|
||||
mock_token = (
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRiMjEyZDdhZj"
|
||||
@@ -181,18 +190,21 @@ def test_get_session_from_platform_token():
|
||||
|
||||
def test_disconnect():
|
||||
"""Test disconnect."""
|
||||
|
||||
with patch(
|
||||
"openbb_core.app.service.hub_service.get",
|
||||
"requests.get",
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {"success": True},
|
||||
),
|
||||
), patch.object(
|
||||
HubService,
|
||||
"_post_logout",
|
||||
return_value=True,
|
||||
):
|
||||
mock_hub_session = MagicMock(
|
||||
spec=HubSession, access_token=SecretStr("token"), token_type="Bearer"
|
||||
)
|
||||
hub_service = HubService(session=mock_hub_session)
|
||||
hub_service = HubService(mock_hub_session)
|
||||
|
||||
assert hub_service.disconnect() is True
|
||||
assert hub_service.session is None
|
||||
@@ -201,17 +213,21 @@ def test_disconnect():
|
||||
def test_get_user_settings():
|
||||
"""Test get user settings."""
|
||||
with patch(
|
||||
"openbb_core.app.service.hub_service.get",
|
||||
"requests.get",
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {},
|
||||
),
|
||||
), patch.object(
|
||||
HubService,
|
||||
"_get_user_settings",
|
||||
return_value=MagicMock(spec=HubUserSettings),
|
||||
):
|
||||
mock_hub_session = MagicMock(
|
||||
spec=HubSession, access_token=SecretStr("token"), token_type="Bearer"
|
||||
)
|
||||
|
||||
user_settings = HubService()._get_user_settings(mock_hub_session)
|
||||
hub_service = HubService(mock_hub_session)
|
||||
user_settings = hub_service._get_user_settings()
|
||||
assert isinstance(user_settings, HubUserSettings)
|
||||
|
||||
|
||||
@@ -219,19 +235,23 @@ def test_put_user_settings():
|
||||
"""Test put user settings."""
|
||||
|
||||
with patch(
|
||||
"openbb_core.app.service.hub_service.put",
|
||||
"requests.put",
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
),
|
||||
), patch.object(
|
||||
HubService,
|
||||
"_put_user_settings",
|
||||
return_value=True,
|
||||
):
|
||||
mock_hub_session = MagicMock(
|
||||
spec=HubSession, access_token=SecretStr("token"), token_type="Bearer"
|
||||
)
|
||||
mock_user_settings = MagicMock(spec=HubUserSettings)
|
||||
|
||||
hub_service = HubService(mock_hub_session)
|
||||
assert (
|
||||
HubService()._put_user_settings(mock_hub_session, mock_user_settings)
|
||||
is True
|
||||
hub_service._put_user_settings(mock_hub_session, mock_user_settings) is True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,13 +26,40 @@ from pydantic import BaseModel, ConfigDict
|
||||
# pylint: disable=W0613, W0621, W0102, W0212
|
||||
|
||||
|
||||
class MockAPIRoute:
|
||||
"""MockAPIRoute"""
|
||||
|
||||
def __init__(self, route):
|
||||
"""Initialize the mock API route."""
|
||||
self.route = route
|
||||
self.openapi_extra = {"no_validate": True}
|
||||
|
||||
|
||||
class MockExecutionContext:
|
||||
"""MockExecutionContext"""
|
||||
|
||||
_route_map = {"mock/route": "mock_func"}
|
||||
|
||||
def __init__(self, cmd_map, route, sys, user):
|
||||
"""Initialize the mock execution context."""
|
||||
self.command_map = cmd_map
|
||||
self.route = route
|
||||
self.system_settings = sys
|
||||
self.user_settings = user
|
||||
|
||||
@property
|
||||
def api_route(self) -> str:
|
||||
"""Mock API route."""
|
||||
return MockAPIRoute(self.route)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def execution_context():
|
||||
"""Set up execution context."""
|
||||
sys = SystemSettings()
|
||||
user = UserSettings()
|
||||
cmd_map = CommandMap()
|
||||
return ExecutionContext(cmd_map, "mock/route", sys, user)
|
||||
return MockExecutionContext(cmd_map, "mock/route", sys, user)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -304,6 +331,7 @@ async def test_static_command_runner_run(
|
||||
self.results = results
|
||||
self.extra = {}
|
||||
self.extra["metadata"] = {"test": "test"}
|
||||
self.provider = None
|
||||
|
||||
mock_get_command.return_value = other_mock_func
|
||||
mock_execute_func.return_value = MockOBBject(results=[1, 2, 3, 4])
|
||||
@@ -330,12 +358,7 @@ async def test_static_command_runner_execute_func(
|
||||
):
|
||||
"""Test execute_func."""
|
||||
|
||||
class MockOBBject:
|
||||
"""Mock OBBject"""
|
||||
|
||||
def __init__(self, results):
|
||||
self.results = results
|
||||
self.extra = {}
|
||||
static_command_runner = StaticCommandRunner()
|
||||
|
||||
mock_parameters_builder_build.return_value = {
|
||||
"a": 1,
|
||||
@@ -346,10 +369,14 @@ async def test_static_command_runner_execute_func(
|
||||
"chart": True,
|
||||
}
|
||||
mock_logging_service.log.return_value = None
|
||||
mock_command.return_value = MockOBBject(results=[1, 2, 3, 4])
|
||||
mock_command.return_value = OBBject(
|
||||
results=[1, 2, 3, 4],
|
||||
provider="mock_provider",
|
||||
accessors={"charting": Mock()},
|
||||
)
|
||||
mock_chart.return_value = None
|
||||
|
||||
result = await StaticCommandRunner._execute_func(
|
||||
result = await static_command_runner._execute_func(
|
||||
"mock/route", (1, 2, 3, 4), execution_context, mock_func, {"chart": True}
|
||||
)
|
||||
|
||||
@@ -369,6 +396,7 @@ def test_static_command_runner_chart():
|
||||
{"date": "1991", "value": 200},
|
||||
{"date": "1992", "value": 300},
|
||||
],
|
||||
provider="mock_provider",
|
||||
accessors={"charting": Mock()},
|
||||
)
|
||||
mock_obbject.charting.show = Mock()
|
||||
@@ -385,9 +413,10 @@ async def test_static_command_runner_command():
|
||||
class MockOBBject:
|
||||
"""Mock OBBject"""
|
||||
|
||||
def __init__(self, results):
|
||||
def __init__(self, results, **kwargs):
|
||||
self.results = results
|
||||
self.extra = {}
|
||||
self.provider = kwargs.get("provider_choices").provider
|
||||
|
||||
class MockProviderChoices:
|
||||
"""Mock ProviderChoices"""
|
||||
@@ -396,7 +425,7 @@ async def test_static_command_runner_command():
|
||||
self.provider = provider
|
||||
|
||||
def other_mock_func(**kwargs):
|
||||
return MockOBBject(results=[1, 2, 3, 4])
|
||||
return MockOBBject([1, 2, 3, 4], **kwargs)
|
||||
|
||||
mock_provider_choices = MockProviderChoices(provider="mock_provider")
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from openbb_core.app.provider_interface import (
|
||||
)
|
||||
from openbb_core.app.router import (
|
||||
CommandMap,
|
||||
CommandValidator,
|
||||
Router,
|
||||
RouterLoader,
|
||||
SignatureInspector,
|
||||
@@ -30,106 +29,6 @@ class MockBaseModel(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def command_validator():
|
||||
"""Set up command_validator."""
|
||||
return CommandValidator()
|
||||
|
||||
|
||||
def test_command_validator_init(command_validator):
|
||||
"""Test init."""
|
||||
assert command_validator
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_, expected",
|
||||
[
|
||||
(str, True),
|
||||
(int, True),
|
||||
(float, True),
|
||||
(bool, True),
|
||||
(list, True),
|
||||
(MockBaseModel, False),
|
||||
],
|
||||
)
|
||||
def test_is_standard_pydantic_type(command_validator, type_, expected):
|
||||
"""Test is_standard_pydantic_type."""
|
||||
# assert command_validator.is_standard_pydantic_type(str)
|
||||
assert command_validator.is_standard_pydantic_type(type_) == expected
|
||||
|
||||
|
||||
def test_is_valid_pydantic_model_type(command_validator):
|
||||
"""Test is_valid_pydantic_model_type."""
|
||||
assert command_validator.is_valid_pydantic_model_type(MockBaseModel)
|
||||
assert not command_validator.is_valid_pydantic_model_type(str)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_, expected",
|
||||
[
|
||||
(str, True),
|
||||
(int, True),
|
||||
(float, True),
|
||||
(bool, True),
|
||||
(list, True),
|
||||
(MockBaseModel, True),
|
||||
],
|
||||
)
|
||||
def test_is_serializable_value_type(command_validator, type_, expected):
|
||||
"""Test is_serializable_value_type."""
|
||||
assert command_validator.is_serializable_value_type(type_) == expected
|
||||
|
||||
|
||||
def test_is_annotated_dc(command_validator):
|
||||
"""Test is_annotated_dc."""
|
||||
assert not command_validator.is_annotated_dc(str)
|
||||
|
||||
|
||||
def test_check_reserved_param(command_validator):
|
||||
"""Test check_reserved_param."""
|
||||
assert command_validator.check_reserved_param("name", str, {}, str, str) is None
|
||||
assert not command_validator.check_reserved_param("name", str, {}, str, int)
|
||||
|
||||
|
||||
def test_check_parameters(command_validator):
|
||||
"""Test check_parameters."""
|
||||
|
||||
async def func():
|
||||
pass
|
||||
|
||||
assert command_validator.check_parameters(func) is None
|
||||
with pytest.raises(TypeError):
|
||||
command_validator.check_parameters(MockBaseModel)
|
||||
|
||||
|
||||
def test_check_return_error(command_validator):
|
||||
"""Test check_return fail."""
|
||||
with pytest.raises(TypeError):
|
||||
|
||||
async def func():
|
||||
pass
|
||||
|
||||
command_validator.check_return(func)
|
||||
|
||||
|
||||
def test_check_return(command_validator):
|
||||
"""Test check_return."""
|
||||
|
||||
async def valid_function() -> OBBject[Optional[List[int]]]:
|
||||
return OBBject(results=[1, 2, 3])
|
||||
|
||||
assert command_validator.check_return(valid_function) is None
|
||||
|
||||
|
||||
def test_check(command_validator):
|
||||
"""Test check."""
|
||||
|
||||
async def valid_function() -> OBBject[Optional[List[int]]]:
|
||||
return OBBject(results=[1, 2, 3])
|
||||
|
||||
assert command_validator.check(valid_function) is None
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def router():
|
||||
"""Set up router."""
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Test the provider helpers."""
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from openbb_core.provider.utils.client import ClientSession
|
||||
from openbb_core.provider.utils.helpers import (
|
||||
amake_request,
|
||||
amake_requests,
|
||||
get_querystring,
|
||||
get_requests_session,
|
||||
make_request,
|
||||
to_snake_case,
|
||||
)
|
||||
@@ -82,9 +82,10 @@ def test_make_request(monkeypatch):
|
||||
"""Mock the requests.get method."""
|
||||
return MockResponse()
|
||||
|
||||
monkeypatch.setattr(requests, "get", mock_get)
|
||||
client_session = get_requests_session()
|
||||
monkeypatch.setattr(client_session, "get", mock_get)
|
||||
|
||||
response = make_request("http://mock.url")
|
||||
response = make_request("http://mock.url", session=client_session)
|
||||
assert response.status_code == 200
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
Reference in New Issue
Block a user