summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormontezdesousa <79287829+montezdesousa@users.noreply.github.com>2024-05-20 09:16:21 +0100
committerGitHub <noreply@github.com>2024-05-20 08:16:21 +0000
commitbe75bfed9c5b63107ce8ff785aa55a75a80612a5 (patch)
tree0dc6d98bed4a25f6b8b05ea36d89ae076239c6d7
parentef311a1fbff6a4235dc919fb4b3f534237163780 (diff)
[Feature] Improve Exception handlers (#6430)
* feat: modify error handlers * fix: clean python errors * fix: return error kind * fix: clean decorator * fix: query validation error kind * fix: no kind, API * fix: no kind, python * fix: rename error * lint * update docs * comment * typo * rename handler * rename base handler --------- Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
-rw-r--r--openbb_platform/CONTRIBUTING.md11
-rw-r--r--openbb_platform/core/openbb_core/api/app_loader.py44
-rw-r--r--openbb_platform/core/openbb_core/api/exception_handlers.py69
-rw-r--r--openbb_platform/core/openbb_core/api/rest_api.py41
-rw-r--r--openbb_platform/core/openbb_core/app/command_runner.py8
-rw-r--r--openbb_platform/core/openbb_core/app/logs/logging_service.py13
-rw-r--r--openbb_platform/core/openbb_core/app/static/utils/decorators.py48
-rw-r--r--openbb_platform/core/openbb_core/provider/utils/errors.py4
-rw-r--r--website/content/excel/index.md5
9 files changed, 140 insertions, 103 deletions
diff --git a/openbb_platform/CONTRIBUTING.md b/openbb_platform/CONTRIBUTING.md
index 04cf075af2e..62a2806e600 100644
--- a/openbb_platform/CONTRIBUTING.md
+++ b/openbb_platform/CONTRIBUTING.md
@@ -37,6 +37,7 @@
- [Important classes](#important-classes)
- [Import statements](#import-statements)
- [The TET pattern](#the-tet-pattern)
+ - [Error](#errors)
- [Data processing commands](#data-processing-commands)
- [Python Interface](#python-interface)
- [API Interface](#api-interface)
@@ -538,6 +539,16 @@ As the OpenBB Platform has its own standardization framework and the data fetche
2. Extract - `extract_data(query: ExampleQueryParams,credentials: Optional[Dict[str, str]],**kwargs: Any,) -> Dict`: makes the request to the API endpoint and returns the raw data. Given the transformed query parameters, the credentials and any other extra arguments, this method should return the raw data as a dictionary.
3. Transform - `transform_data(query: ExampleQueryParams, data: Dict, **kwargs: Any) -> List[ExampleHistoricalData]`: transforms the raw data into the defined data model. Given the transformed query parameters (might be useful for some filtering), the raw data and any other extra arguments, this method should return the transformed data as a list of [`Data`](openbb_platform/platform/provider/openbb_core/provider/abstract/data.py) children.
+#### Errors
+
+To ensure a consistent error handling behavior our API relies on the convention below.
+
+| Status code | Exception | Detail | Description |
+| -------- | ------- | ------- | ------- |
+| 400 | `OpenBBError` or child of `OpenBBError` | Custom message. | Use this to explicitly raise custom exceptions, like `EmptyDataError`. |
+| 422 | `ValidationError` | `Pydantic` errors dict message. | Automatically raised to inform the user about query validation errors. ValidationErrors outside of the query are treated with status code 500 by default. |
+| 500 | Any exception not covered above, eg `ValueError`, `ZeroDivisionError` | Unexpected error. | Unexpected exceptions, most likely a bug. |
+
#### Data processing commands
The data processing commands are commands that are used to process the data that may or may not come from the OpenBB Platform.
diff --git a/openbb_platform/core/openbb_core/api/app_loader.py b/openbb_platform/core/openbb_core/api/app_loader.py
index d6ff19878ca..a22594a8534 100644
--- a/openbb_platform/core/openbb_core/api/app_loader.py
+++ b/openbb_platform/core/openbb_core/api/app_loader.py
@@ -3,34 +3,38 @@
from typing import List, Optional
from fastapi import APIRouter, FastAPI
+from openbb_core.api.exception_handlers import ExceptionHandlers
+from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.router import RouterLoader
+from pydantic import ValidationError
class AppLoader:
"""App loader."""
@staticmethod
- def get_openapi_tags() -> List[dict]:
- """Get openapi tags."""
- main_router = RouterLoader.from_extensions()
- openapi_tags = []
- # Add tag data for each router in the main router
- for r in main_router.routers:
- openapi_tags.append(
- {
- "name": r,
- "description": main_router.get_attr(r, "description"),
- }
- )
- return openapi_tags
-
- @staticmethod
- def from_routers(
- app: FastAPI, routers: List[Optional[APIRouter]], prefix: str
- ) -> FastAPI:
- """Load routers to app."""
+ def add_routers(app: FastAPI, routers: List[Optional[APIRouter]], prefix: str):
+ """Add routers."""
for router in routers:
if router:
app.include_router(router=router, prefix=prefix)
- return app
+ @staticmethod
+ def add_openapi_tags(app: FastAPI):
+ """Add openapi tags."""
+ main_router = RouterLoader.from_extensions()
+ # Add tag data for each router in the main router
+ app.openapi_tags = [
+ {
+ "name": r,
+ "description": main_router.get_attr(r, "description"),
+ }
+ for r in main_router.routers
+ ]
+
+ @staticmethod
+ def add_exception_handlers(app: FastAPI):
+ """Add exception handlers."""
+ app.exception_handlers[Exception] = ExceptionHandlers.exception
+ app.exception_handlers[ValidationError] = ExceptionHandlers.validation
+ app.exception_handlers[OpenBBError] = ExceptionHandlers.openbb
diff --git a/openbb_platform/core/openbb_core/api/exception_handlers.py b/openbb_platform/core/openbb_core/api/exception_handlers.py
new file mode 100644
index 00000000000..7b9a3d833ba
--- /dev/null
+++ b/openbb_platform/core/openbb_core/api/exception_handlers.py
@@ -0,0 +1,69 @@
+"""Exception handlers module."""
+
+import logging
+from typing import Any
+
+from fastapi import Request
+from fastapi.responses import JSONResponse
+from openbb_core.app.model.abstract.error import OpenBBError
+from openbb_core.env import Env
+from pydantic import ValidationError
+
+logger = logging.getLogger("uvicorn.error")
+
+
+class ExceptionHandlers:
+ """Exception handlers."""
+
+ @staticmethod
+ async def _handle(exception: Exception, status_code: int, detail: Any):
+ """Exception handler."""
+ if Env().DEBUG_MODE:
+ raise exception
+ logger.error(exception)
+ return JSONResponse(
+ status_code=status_code,
+ content={
+ "detail": detail,
+ },
+ )
+
+ @staticmethod
+ async def exception(_: Request, error: Exception) -> JSONResponse:
+ """Exception handler for Base Exception."""
+ return await ExceptionHandlers._handle(
+ exception=error,
+ status_code=500,
+ detail="Unexpected error.",
+ )
+
+ @staticmethod
+ async def validation(request: Request, error: ValidationError):
+ """Exception handler for ValidationError."""
+ # Some validation is performed at Fetcher level.
+ # So we check if the validation error comes from a QueryParams class.
+ # And that it is in the request query params.
+ # 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)
+ all_in_query = all(
+ loc in query_params for err in errors for loc in err.get("loc", ())
+ )
+ if "QueryParams" in error.title and all_in_query:
+ detail = [{**err, "loc": ("query",) + err.get("loc", ())} for err in errors]
+ return await ExceptionHandlers._handle(
+ exception=error,
+ status_code=422,
+ detail=detail,
+ )
+ return await ExceptionHandlers.exception(request, error)
+
+ @staticmethod
+ async def openbb(_: Request, error: OpenBBError):
+ """Exception handler for OpenBBError."""
+ return await ExceptionHandlers._handle(
+ exception=error,
+ status_code=400,
+ detail=str(error.original),
+ )
diff --git a/openbb_platform/core/openbb_core/api/rest_api.py b/openbb_platform/core/openbb_core/api/rest_api.py
index 0ad92f613b6..e74b8774121 100644
--- a/openbb_platform/core/openbb_core/api/rest_api.py
+++ b/openbb_platform/core/openbb_core/api/rest_api.py
@@ -3,14 +3,12 @@
import logging
from contextlib import asynccontextmanager
-from fastapi import FastAPI, Request
+from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse
from openbb_core.api.app_loader import AppLoader
from openbb_core.api.router.commands import router as router_commands
from openbb_core.api.router.coverage import router as router_coverage
from openbb_core.api.router.system import router as router_system
-from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.service.auth_service import AuthService
from openbb_core.app.service.system_service import SystemService
from openbb_core.env import Env
@@ -73,8 +71,7 @@ app.add_middleware(
allow_methods=system.api_settings.cors.allow_methods,
allow_headers=system.api_settings.cors.allow_headers,
)
-app.openapi_tags = AppLoader.get_openapi_tags()
-AppLoader.from_routers(
+AppLoader.add_routers(
app=app,
routers=(
[AuthService().router, router_system, router_coverage, router_commands]
@@ -83,38 +80,8 @@ AppLoader.from_routers(
),
prefix=system.api_settings.prefix,
)
-
-
-@app.exception_handler(Exception)
-async def api_exception_handler(_: Request, exc: Exception):
- """Exception handler for all other exceptions."""
- if Env().DEBUG_MODE:
- raise exc
- logger.error(exc)
- return JSONResponse(
- status_code=404,
- content={
- "detail": str(exc),
- "error_kind": exc.__class__.__name__,
- },
- )
-
-
-@app.exception_handler(OpenBBError)
-async def openbb_exception_handler(_: Request, exc: OpenBBError):
- """Exception handler for OpenBB errors."""
- if Env().DEBUG_MODE:
- raise exc
- logger.error(exc.original)
- openbb_error = exc.original
- status_code = 400 if "No results" in str(openbb_error) else 500
- return JSONResponse(
- status_code=status_code,
- content={
- "detail": str(openbb_error),
- "error_kind": openbb_error.__class__.__name__,
- },
- )
+AppLoader.add_openapi_tags(app)
+AppLoader.add_exception_handlers(app)
if __name__ == "__main__":
diff --git a/openbb_platform/core/openbb_core/app/command_runner.py b/openbb_platform/core/openbb_core/app/command_runner.py
index cf74f75c87a..7f2eb595765 100644
--- a/openbb_platform/core/openbb_core/app/command_runner.py
+++ b/openbb_platform/core/openbb_core/app/command_runner.py
@@ -229,7 +229,8 @@ class ParametersBuilder:
}
# We allow extra fields to return with model with 'cc: CommandContext'
config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
- ValidationModel = create_model(func.__name__, __config__=config, **fields) # type: ignore # pylint: disable=C0103
+ # pylint: disable=C0103
+ ValidationModel = create_model(func.__name__, __config__=config, **fields) # type: ignore
# Validate and coerce
model = ValidationModel(**kwargs)
ParametersBuilder._warn_kwargs(
@@ -331,7 +332,7 @@ class StaticCommandRunner:
if chart_params:
kwargs.update(chart_params)
- obbject.charting.show(render=False, **kwargs)
+ obbject.charting.show(render=False, **kwargs) # type: ignore[attr-defined]
except Exception as e: # pylint: disable=broad-exception-caught
if Env().DEBUG_MODE:
raise OpenBBError(e) from e
@@ -386,9 +387,6 @@ class StaticCommandRunner:
obbject._standard_params = kwargs.get("standard_params", None)
if chart and obbject.results:
cls._chart(obbject, **kwargs)
-
- except Exception as e:
- raise OpenBBError(e) from e
finally:
ls = LoggingService(system_settings, user_settings)
ls.log(
diff --git a/openbb_platform/core/openbb_core/app/logs/logging_service.py b/openbb_platform/core/openbb_core/app/logs/logging_service.py
index abc605ca7d8..42c4b1c8b83 100644
--- a/openbb_platform/core/openbb_core/app/logs/logging_service.py
+++ b/openbb_platform/core/openbb_core/app/logs/logging_service.py
@@ -4,14 +4,13 @@ import json
import logging
from enum import Enum
from types import TracebackType
-from typing import Any, Callable, Dict, Optional, Tuple, Type, Union, cast
+from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
FormatterWithExceptions,
)
from openbb_core.app.logs.handlers_manager import HandlersManager
from openbb_core.app.logs.models.logging_settings import LoggingSettings
-from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.abstract.singleton import SingletonMeta
from openbb_core.app.model.system_settings import SystemSettings
from openbb_core.app.model.user_settings import UserSettings
@@ -224,24 +223,22 @@ class LoggingService(metaclass=SingletonMeta):
kwargs = {k: str(v)[:100] for k, v in kwargs.items()}
# Get execution info
- openbb_error = cast(
- Optional[OpenBBError], exec_info[1] if exec_info else None
- )
+ error = str(exec_info[1]) if exec_info and len(exec_info) > 1 else None
# Construct message
- message_label = "ERROR" if openbb_error else "CMD"
+ message_label = "ERROR" if error else "CMD"
log_message = json.dumps(
{
"route": route,
"input": kwargs,
- "error": str(openbb_error.original) if openbb_error else None,
+ "error": error,
"custom_headers": custom_headers,
},
default=to_jsonable_python,
)
log_message = f"{message_label}: {log_message}"
- log_level = logger.error if openbb_error else logger.info
+ log_level = logger.error if error else logger.info
log_level(
log_message,
extra={"func_name_override": func.__name__},
diff --git a/openbb_platform/core/openbb_core/app/static/utils/decorators.py b/openbb_platform/core/openbb_core/app/static/utils/decorators.py
index a8fbf1f0d59..8daefae0575 100644
--- a/openbb_platform/core/openbb_core/app/static/utils/decorators.py
+++ b/openbb_platform/core/openbb_core/app/static/utils/decorators.py
@@ -47,8 +47,7 @@ def exception_handler(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*f_args, **f_kwargs):
try:
return func(*f_args, **f_kwargs)
- except (ValidationError, Exception) as e:
- # If the DEBUG_MODE is enabled, raise the exception with complete traceback
+ except (ValidationError, OpenBBError, Exception) as e:
if Env().DEBUG_MODE:
raise
@@ -60,34 +59,27 @@ def exception_handler(func: Callable[P, R]) -> Callable[P, R]:
if isinstance(e, ValidationError):
error_list = []
-
- validation_error = f"{e.error_count()} validations errors in {e.title}"
- for error in e.errors():
- arg = ".".join(map(str, error["loc"]))
- arg_error = f"Arg {arg} ->\n"
- error_details = (
- f"{error['msg']} "
- f"[validation_error_type={error['type']}, "
- f"input_type={type(error['input']).__name__}, "
- f"input_value={error['input']}]\n"
- )
- url = error.get("url")
- error_info = (
- f" For further information visit {url}\n" if url else ""
+ validation_error = f"{e.error_count()} validations error(s)"
+ for err in e.errors(include_url=False):
+ loc = ".".join(
+ [
+ str(i)
+ for i in err.get("loc", ())
+ if i not in ("standard_params", "extra_params")
+ ]
)
- error_list.append(arg_error + error_details + error_info)
-
+ _input = err.get("input", "")
+ msg = err.get("msg", "")
+ error_list.append(f"[Arg] {loc} -> input: {_input} -> {msg}")
error_list.insert(0, validation_error)
error_str = "\n".join(error_list)
-
- raise OpenBBError(
- f"\nType -> ValidationError \n\nDetails -> {error_str}"
- ).with_traceback(tb) from None
-
- # If the error is not a ValidationError, then it is a generic exception
- error_type = getattr(e, "original", e).__class__.__name__
- raise OpenBBError(
- f"\nType -> {error_type}\n\nDetail -> {str(e)}"
- ).with_traceback(tb) from None
+ raise OpenBBError(f"\n[Error] -> {error_str}").with_traceback(
+ tb
+ ) from None
+ if isinstance(e, OpenBBError):
+ raise OpenBBError(f"\n[Error] -> {str(e)}").with_traceback(tb) from None
+ raise OpenBBError("\n[Error] -> Unexpected error.").with_traceback(
+ tb
+ ) from None
return wrapper
diff --git a/openbb_platform/core/openbb_core/provider/utils/errors.py b/openbb_platform/core/openbb_core/provider/utils/errors.py
index 2faa03e2d8b..cfa87f16c37 100644
--- a/openbb_platform/core/openbb_core/provider/utils/errors.py
+++ b/openbb_platform/core/openbb_core/provider/utils/errors.py
@@ -1,7 +1,9 @@
"""Custom exceptions for the provider."""
+from openbb_core.app.model.abstract.error import OpenBBError
-class EmptyDataError(Exception):
+
+class EmptyDataError(OpenBBError):
"""Exception raised for empty data."""
def __init__(
diff --git a/website/content/excel/index.md b/website/content/excel/index.md
index bd8232b7e78..2540b742223 100644
--- a/website/content/excel/index.md
+++ b/website/content/excel/index.md
@@ -29,14 +29,11 @@ Data standardization is at the core of OpenBB Add-in for Excel, offering you a c
<button
className="bg-grey-200 hover:bg-grey-400 dark:bg-[#303038] dark:hover:bg-grey-600 text-grey-900 dark:text-grey-200 text-sm font-medium py-2 px-4 rounded"
>
- Join Add-In for Excel waitlist
+ Start Terminal Pro free-trial
</button>
</a>
</div>
-
-Data standardization is at the core of OpenBB for Excel, offering you a consistent and reliable dataset from a diverse range of asset classes, from equity, fixed income, and cryptocurrency to macroeconomics. This seamless fetch of data means you can readily compare across providers and update it instantly, ensuring accuracy and saving you valuable time.
-
---
- **Features**