Skip to content

Commit

Permalink
[BugFix] Add Exception Handling For Unauthorized API Key Error. (#6800)
Browse files Browse the repository at this point in the history
* add exception handling for unauthorized api call request.

* linting

* more linting

* more linting

* missed files

* biztoc cassette

* more linting

* crypto interval choices as 7d and 30d instead of 5 and 21

* update the default message and a few Intrinio endpoints to use the unauthorized error.

* mypy

* no-else-raise

---------

Co-authored-by: Theodore Aptekarev <[email protected]>
  • Loading branch information
deeleeramone and piiq authored Oct 23, 2024
1 parent f8a45fa commit 258e158
Show file tree
Hide file tree
Showing 34 changed files with 935 additions and 580 deletions.
3 changes: 2 additions & 1 deletion openbb_platform/core/openbb_core/api/app_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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 openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
from pydantic import ValidationError


Expand Down Expand Up @@ -40,3 +40,4 @@ def add_exception_handlers(app: FastAPI):
app.exception_handlers[ValidationError] = ExceptionHandlers.validation
app.exception_handlers[OpenBBError] = ExceptionHandlers.openbb
app.exception_handlers[EmptyDataError] = ExceptionHandlers.empty_data
app.exception_handlers[UnauthorizedError] = ExceptionHandlers.unauthorized
13 changes: 11 additions & 2 deletions openbb_platform/core/openbb_core/api/exception_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from fastapi.responses import JSONResponse, Response
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.env import Env
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
from pydantic import ValidationError

logger = logging.getLogger("uvicorn.error")
Expand Down Expand Up @@ -55,7 +55,7 @@ async def exception(_: Request, error: Exception) -> JSONResponse:
return await ExceptionHandlers._handle(
exception=error,
status_code=500,
detail="Unexpected error.",
detail=f"Unexpected Error -> {error.__class__.__name__} -> {str(error.args[0] or error.args)}",
)

@staticmethod
Expand Down Expand Up @@ -99,3 +99,12 @@ async def openbb(_: Request, error: OpenBBError):
async def empty_data(_: Request, error: EmptyDataError):
"""Exception handler for EmptyDataError."""
return Response(status_code=204)

@staticmethod
async def unauthorized(_: Request, error: UnauthorizedError):
"""Exception handler for OpenBBError."""
return await ExceptionHandlers._handle(
exception=error,
status_code=502,
detail=str(error.original),
)
4 changes: 4 additions & 0 deletions openbb_platform/core/openbb_core/app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ def command(
"model": OpenBBErrorResponse,
"description": "Internal Error",
},
502: {
"model": OpenBBErrorResponse,
"description": "Unauthorized",
},
},
)

Expand Down
18 changes: 15 additions & 3 deletions openbb_platform/core/openbb_core/app/static/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.env import Env
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
from pydantic import ValidationError, validate_call
from typing_extensions import ParamSpec

Expand Down Expand Up @@ -85,10 +86,21 @@ def wrapper(*f_args, **f_kwargs):
raise OpenBBError(f"\n[Error] -> {error_str}").with_traceback(
tb
) from None
if isinstance(e, UnauthorizedError):
raise UnauthorizedError(f"\n[Error] -> {str(e)}").with_traceback(
tb
) from None
if isinstance(e, EmptyDataError):
raise EmptyDataError(f"\n[Empty] -> {str(e)}").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
if isinstance(e, Exception):
raise OpenBBError(
f"\n[Unexpected Error] -> {e.__class__.__name__} -> {str(e.args[0] or e.args)}"
).with_traceback(tb) from None

return None

return wrapper
25 changes: 25 additions & 0 deletions openbb_platform/core/openbb_core/provider/utils/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Custom exceptions for the provider."""

from typing import Union

from openbb_core.app.model.abstract.error import OpenBBError


Expand All @@ -12,3 +14,26 @@ def __init__(
"""Initialize the exception."""
self.message = message
super().__init__(self.message)


class UnauthorizedError(OpenBBError):
"""Exception raised for an unauthorized provider request response."""

def __init__(
self,
message: Union[str, tuple[str]] = (
"Unauthorized <provider name> API request."
" Please check your <provider name> credentials and subscription access.",
),
provider_name: str = "<provider name>",
):
"""Initialize the exception."""
if provider_name and provider_name != "<provider name>":
msg = message
if isinstance(msg, tuple):
msg = msg[0].replace("<provider name>", provider_name)
elif isinstance(msg, str):
msg = msg.replace("<provider name>", provider_name)
message = msg
self.message = message
super().__init__(self.message)
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@
)
from typing import Any, Dict, List, Optional

from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.analyst_search import (
AnalystSearchData,
AnalystSearchQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.helpers import (
amake_request,
get_querystring,
safe_fromtimestamp,
)
from pydantic import Field, field_validator, model_validator


Expand Down Expand Up @@ -367,6 +363,9 @@ class BenzingaAnalystSearchData(AnalystSearchData):
@classmethod
def validate_date(cls, v: float) -> Optional[dateType]:
"""Validate last_updated."""
# pylint: disable=import-outside-toplevel
from openbb_core.provider.utils.helpers import safe_fromtimestamp

if v:
dt = safe_fromtimestamp(v, tz=timezone.utc)
return dt.date() if dt.time() == dt.min.time() else dt
Expand Down Expand Up @@ -414,15 +413,31 @@ async def aextract_data(
**kwargs: Any,
) -> List[Dict]:
"""Extract the raw data."""
# pylint: disable=import-outside-toplevel
from openbb_benzinga.utils.helpers import response_callback
from openbb_core.provider.utils.helpers import amake_request, get_querystring

token = credentials.get("benzinga_api_key") if credentials else ""
querystring = get_querystring(query.model_dump(), [])
querystring = get_querystring(query.model_dump(by_alias=True), [])
url = f"https://api.benzinga.com/api/v2.1/calendar/ratings/analysts?{querystring}&token={token}"
response = await amake_request(url, **kwargs)
data = await amake_request(url, response_callback=response_callback, **kwargs)

if (isinstance(data, list) and not data) or (
isinstance(data, dict) and not data.get("analyst_ratings_analyst")
):
raise EmptyDataError("No ratings data returned.")

if isinstance(data, dict) and "analyst_ratings_analyst" not in data:
raise OpenBBError(
f"Unexpected data format. Expected 'analyst_ratings_analyst' key, got: {list(data.keys())}"
)

if not response:
raise EmptyDataError()
if not isinstance(data, dict):
raise OpenBBError(
f"Unexpected data format. Expected dict, got: {type(data).__name__}"
)

return response.get("analyst_ratings_analyst") # type: ignore
return data["analyst_ratings_analyst"]

@staticmethod
def transform_data(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
"""Benzinga Company News Model."""

import math
# pylint: disable=unused-argument

from datetime import (
date as dateType,
datetime,
)
from typing import Any, Dict, List, Literal, Optional

from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.company_news import (
CompanyNewsData,
CompanyNewsQueryParams,
)
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_core.provider.utils.helpers import amake_requests, get_querystring
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
from pydantic import Field, field_validator


Expand Down Expand Up @@ -148,6 +150,12 @@ async def aextract_data(
**kwargs: Any,
) -> List[Dict]:
"""Extract data."""
# pylint: disable=import-outside-toplevel
import asyncio # noqa
import math
from openbb_core.provider.utils.helpers import amake_request, get_querystring
from openbb_benzinga.utils.helpers import response_callback

token = credentials.get("benzinga_api_key") if credentials else ""

base_url = "https://api.benzinga.com/api/v2/news"
Expand All @@ -165,11 +173,28 @@ async def aextract_data(
for page in range(pages)
]

data = await amake_requests(urls, **kwargs)
results: list = []

async def get_one(url):
"""Get data for one url."""
try:
response = await amake_request(
url, response_callback=response_callback, **kwargs
)
if response:
results.extend(response)
except (OpenBBError, UnauthorizedError) as e:
raise e from e

await asyncio.gather(*[get_one(url) for url in urls])

if not results:
raise EmptyDataError("The request was returned empty.")

return data[: query.limit]
return sorted(
results, key=lambda x: x.get("created"), reverse=query.order == "desc"
)[: query.limit if query.limit else len(results)]

# pylint: disable=unused-argument
@staticmethod
def transform_data(
query: BenzingaCompanyNewsQueryParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,14 @@
)
from typing import Any, Dict, List, Literal, Optional, Union

from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.price_target import (
PriceTargetData,
PriceTargetQueryParams,
)
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.helpers import (
amake_requests,
get_querystring,
safe_fromtimestamp,
)
from pydantic import Field, field_validator, model_validator

COVERAGE_DICT = {
Expand Down Expand Up @@ -58,10 +54,26 @@ class BenzingaPriceTargetQueryParams(PriceTargetQueryParams):
"firm_ids": "parameters[firm_id]",
}
__json_schema_extra__ = {
"symbol": ["multiple_items_allowed"],
"analyst_ids": ["multiple_items_allowed"],
"firm_ids": ["multiple_items_allowed"],
"fields": ["multiple_items_allowed"],
"symbol": {"multiple_items_allowed": True},
"analyst_ids": {"multiple_items_allowed": True},
"firm_ids": {"multiple_items_allowed": True},
"fields": {"multiple_items_allowed": True},
"action": {
"multiple_items_allowed": False,
"choices": [
"downgrades",
"maintains",
"reinstates",
"reiterates",
"upgrades",
"assumes",
"initiates",
"terminates",
"removes",
"suspends",
"firm_dissolved",
],
},
}

page: Optional[int] = Field(
Expand Down Expand Up @@ -231,6 +243,9 @@ def parse_date(cls, v: str):
@classmethod
def validate_date(cls, v: float) -> Optional[dateType]:
"""Convert the Unix timestamp to a datetime object."""
# pylint: disable=import-outside-toplevel
from openbb_core.provider.utils.helpers import safe_fromtimestamp

if v:
dt = safe_fromtimestamp(v, tz=timezone.utc)
return dt.date() if dt.time() == dt.min.time() else dt
Expand Down Expand Up @@ -263,18 +278,30 @@ async def aextract_data(
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the Benzinga endpoint."""
# pylint: disable=import-outside-toplevel
from openbb_benzinga.utils.helpers import response_callback
from openbb_core.provider.utils.helpers import amake_request, get_querystring

token = credentials.get("benzinga_api_key") if credentials else ""

base_url = "https://api.benzinga.com/api/v2.1/calendar/ratings"
querystring = get_querystring(query.model_dump(by_alias=True), [])

url = f"{base_url}?{querystring}&token={token}"
data = await amake_requests(url, **kwargs)
data = await amake_request(url, response_callback=response_callback, **kwargs)

if not data:
raise EmptyDataError()
if isinstance(data, dict) and "ratings" not in data:
raise OpenBBError(
f"Unexpected data format. Expected 'ratings' key, got: {list(data.keys())}"
)
if not isinstance(data, dict):
raise OpenBBError(
f"Unexpected data format. Expected dict, got: {type(data)}"
)
if isinstance(data, dict) and not data.get("ratings"):
raise EmptyDataError("No ratings data returned.")

return data[0].get("ratings")
return data["ratings"]

@staticmethod
def transform_data(
Expand Down
Loading

0 comments on commit 258e158

Please sign in to comment.