mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 04:31:13 +00:00
[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:
@@ -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", ())
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user