Skip to content

Commit 8c066c4

Browse files
committed
Implement custom exceptions for BCB API errors and update error handling in currency and OData modules
1 parent d39e64b commit 8c066c4

8 files changed

Lines changed: 47 additions & 15 deletions

File tree

bcb/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .exceptions import BCBError, BCBAPIError, CurrencyNotFoundError, SGSError, ODataError
12
from .odata.api import (
23
ODataAPI,
34
Expectativas,

bcb/currency.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import requests
1010
from lxml import html
1111

12+
from .exceptions import BCBAPIError, CurrencyNotFoundError
1213
from .utils import Date, DateInput
1314

1415
"""
@@ -37,7 +38,7 @@ def _currency_id_list() -> pd.DataFrame:
3738
res = requests.get(url1)
3839
if res.status_code != 200:
3940
msg = f"BCB API Request error, status code = {res.status_code}"
40-
raise Exception(msg)
41+
raise BCBAPIError(msg, res.status_code)
4142

4243
doc = html.parse(BytesIO(res.content)).getroot()
4344
xpath = "//select[@name='ChkMoeda']/option"
@@ -183,6 +184,6 @@ def get(
183184
elif groupby == "side":
184185
return df.reorder_levels([1, 0], axis=1).sort_index(axis=1)
185186
else:
186-
raise Exception(f"Unknown side value, use: bid, ask, both")
187+
raise ValueError("Unknown side value, use: bid, ask, both")
187188
else:
188-
raise Exception(f"Currency not found: {symbols}")
189+
raise CurrencyNotFoundError(f"Currency not found: {symbols}")

bcb/exceptions.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class BCBError(Exception):
2+
"""Base exception for all python-bcb errors."""
3+
4+
5+
class BCBAPIError(BCBError):
6+
"""HTTP or API-level error from BCB."""
7+
8+
def __init__(self, message: str, status_code: int | None = None):
9+
super().__init__(message)
10+
self.status_code = status_code
11+
12+
13+
class CurrencyNotFoundError(BCBError):
14+
"""Raised when a requested currency symbol is not found."""
15+
16+
17+
class SGSError(BCBError):
18+
"""Raised for SGS-specific API errors."""
19+
20+
21+
class ODataError(BCBError):
22+
"""Raised for OData query/metadata errors."""

bcb/odata/framework.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import httpx
66
from urllib.parse import quote
77

8+
from bcb.exceptions import ODataError
9+
810

911
# Edm.Boolean
1012
# Edm.Byte
@@ -358,7 +360,7 @@ def __getitem__(self, item):
358360
if fi is not None:
359361
return fi
360362
else:
361-
raise ValueError("Invalid name: " + item)
363+
raise ODataError("Invalid name: " + item)
362364
else:
363365
return es
364366

@@ -427,7 +429,7 @@ def parameters(self, **kwargs):
427429
if arg in self.function_parameters:
428430
self.function_parameters[arg] = kwargs[arg]
429431
else:
430-
raise ValueError(f"Unknown parameter: {arg}")
432+
raise ODataError(f"Unknown parameter: {arg}")
431433
return self
432434

433435
def filter(self, *args):
@@ -489,7 +491,7 @@ def text(self):
489491
for p in self.entity.function.parameters:
490492
val = self.function_parameters[p.name]
491493
if p.required and val is None:
492-
raise ValueError("Parameter not set: " + p.name)
494+
raise ODataError("Parameter not set: " + p.name)
493495
params["@" + p.name] = p.format(val)
494496
qs = "&".join([f"{quote(k)}={quote(str(v))}" for k, v in params.items()])
495497
headers = {"OData-Version": "4.0", "OData-MaxVersion": "4.0"}

bcb/sgs/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pandas as pd
66
import requests
77

8+
from bcb.exceptions import SGSError
89
from bcb.utils import Date, DateInput
910

1011
"""
@@ -181,8 +182,8 @@ def get_json(code: int, start: Optional[DateInput] = None, end: Optional[DateInp
181182
except Exception:
182183
res_json = {}
183184
if "error" in res_json:
184-
raise Exception(f"BCB error: {res_json['error']}")
185+
raise SGSError(f"BCB error: {res_json['error']}")
185186
elif "erro" in res_json:
186-
raise Exception(f"BCB error: {res_json['erro']['detail']}")
187-
raise Exception(f"Download error: code = {code}")
187+
raise SGSError(f"BCB error: {res_json['erro']['detail']}")
188+
raise SGSError(f"Download error: code = {code}")
188189
return res.text

bcb/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __init__(self, d: DateInput, format: str = "%Y-%m-%d") -> None:
2929
elif isinstance(d, date):
3030
pass
3131
else:
32-
raise ValueError()
32+
raise ValueError(f"Unsupported date type: {type(d).__name__}")
3333
self.date: date = d
3434

3535
def format(self, fmts: str = "%Y-%m-%d") -> str:

tests/test_currency.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from datetime import datetime, date
22
import pandas as pd
3+
import pytest
34
from pytest import mark
45
from bcb import currency
6+
from bcb.exceptions import CurrencyNotFoundError
57

68

79
def test_currency_id():
@@ -18,10 +20,10 @@ def test_currency_get_symbol():
1820
assert x is None
1921
x = currency.get("USD", start_date, end_date)
2022
assert isinstance(x, pd.DataFrame)
21-
x = currency.get("ZAR", start_date, end_date)
22-
assert x is None
23-
x = currency.get(["ZAR", "ZZ1"], start_date, end_date)
24-
assert x is None
23+
with pytest.raises(CurrencyNotFoundError):
24+
currency.get("ZAR", start_date, end_date)
25+
with pytest.raises(CurrencyNotFoundError):
26+
currency.get(["ZAR", "ZZ1"], start_date, end_date)
2527

2628

2729
def test_get_valid_currency_list():

tests/test_expectativas.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ def endpoints():
1212

1313

1414
def test_expectativas_date_format(endpoints):
15+
date_columns = {"Data", "dataHoraCotacao", "InicioPeriodo", "FimPeriodo", "DataVigencia"}
1516
for endpoint in endpoints:
1617
query = endpoint.query().limit(1)
1718
data = query.collect()
1819
assert isinstance(data, pd.DataFrame)
1920
assert data.shape[0] == 1
20-
assert isinstance(data["Data"].iloc[0], datetime)
21+
for col in date_columns:
22+
if col in data.columns:
23+
assert isinstance(data[col].iloc[0], datetime)

0 commit comments

Comments
 (0)