Merge branch 'develop' into feature/websockets

This commit is contained in:
Danglewood
2025-01-27 09:18:14 -08:00
committed by GitHub
73 changed files with 7710 additions and 8623 deletions

View File

@@ -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

View File

@@ -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", ())
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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(

View File

@@ -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()

View File

@@ -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
)

View File

@@ -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")

View File

@@ -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."""

View File

@@ -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):