[Feature] Add no_validate Flag To @router.command (#6988)

* add no_validate boolean to router.command kwargs

* package builder

* update a comment

* lint

* lint

* check @router.command kwargs in StaticCommandRunner by adding APIRoute to ExecutionContext

* response_model always None when no_validate is True

* add APIRoute forward reference

* lint

* fix tests and remove unused code in router

* fix flakey logging service test

* add PATCH to, PUT and POST, in MethodDefinition

* update ReferenceGenerator

* lint

* test_logging_service lint flags
This commit is contained in:
Danglewood
2025-01-20 11:49:19 -08:00
committed by GitHub
parent 58b252cbde
commit 49f6936026
9 changed files with 240 additions and 353 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

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

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

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

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

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