Skip to content

Commit 5d39321

Browse files
wilsonfreitasclaude
andcommitted
Add CI/CD workflows and apply black formatting (Phase 6)
- Add .github/workflows/test.yml: run pytest on Python 3.10/3.11/3.12 matrix, skipping integration tests, with coverage reporting to Codecov - Add .github/workflows/lint.yml: black --check and mypy on every push/PR - Scope .github/workflows/sphinx.yml to docs/**, bcb/**, and workflow file changes only (avoids rebuilding docs on unrelated pushes) - Apply black formatting to all 14 files that were out of style All CI checks verified locally: 50 tests pass, coverage 72.1%, mypy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent caf04a5 commit 5d39321

17 files changed

Lines changed: 183 additions & 36 deletions

.github/workflows/lint.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Lint
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
lint:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v4
11+
12+
- name: Set up Python 3.10
13+
uses: actions/setup-python@v5
14+
with:
15+
python-version: "3.10"
16+
17+
- name: Install Poetry
18+
run: pip install poetry
19+
20+
- name: Install dependencies
21+
run: poetry install --with dev
22+
23+
- name: Check formatting
24+
run: poetry run black --check bcb/ tests/
25+
26+
- name: Type check
27+
run: poetry run mypy bcb/

.github/workflows/sphinx.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
name: Sphinx build
22

3-
on: push
3+
on:
4+
push:
5+
paths:
6+
- 'docs/**'
7+
- 'bcb/**'
8+
- '.github/workflows/sphinx.yml'
49

510
jobs:
611
build:

.github/workflows/test.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Tests
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: ["3.10", "3.11", "3.12"]
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python ${{ matrix.python-version }}
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
20+
- name: Install Poetry
21+
run: pip install poetry
22+
23+
- name: Install dependencies
24+
run: poetry install --with test,dev
25+
26+
- name: Run tests
27+
run: poetry run pytest --cov=bcb --cov-report=xml -m "not integration"
28+
29+
- name: Upload coverage
30+
uses: codecov/codecov-action@v4
31+
with:
32+
file: coverage.xml

bcb/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from .exceptions import BCBError, BCBAPIError, CurrencyNotFoundError, SGSError, ODataError
1+
from .exceptions import (
2+
BCBError,
3+
BCBAPIError,
4+
CurrencyNotFoundError,
5+
SGSError,
6+
ODataError,
7+
)
28
from .odata.api import (
39
ODataAPI,
410
Expectativas,

bcb/currency.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ def _currency_id_list() -> pd.DataFrame:
3434
if CACHE.get("TEMP_CURRENCY_ID_LIST") is not None:
3535
return CACHE.get("TEMP_CURRENCY_ID_LIST")
3636
else:
37-
url1 = "https://ptax.bcb.gov.br/ptax_internet/consultaBoletim.do?" "method=exibeFormularioConsultaBoletim"
37+
url1 = (
38+
"https://ptax.bcb.gov.br/ptax_internet/consultaBoletim.do?"
39+
"method=exibeFormularioConsultaBoletim"
40+
)
3841
res = httpx.get(url1, follow_redirects=True)
3942
if res.status_code != 200:
4043
msg = f"BCB API Request error, status code = {res.status_code}"
@@ -106,7 +109,9 @@ def _get_currency_id(symbol: str) -> int:
106109
return int(matches.max())
107110

108111

109-
def _get_symbol(symbol: str, start_date: DateInput, end_date: DateInput) -> Optional[pd.DataFrame]:
112+
def _get_symbol(
113+
symbol: str, start_date: DateInput, end_date: DateInput
114+
) -> Optional[pd.DataFrame]:
110115
try:
111116
cid = _get_currency_id(symbol)
112117
except CurrencyNotFoundError:
@@ -126,7 +131,9 @@ def _get_symbol(symbol: str, start_date: DateInput, end_date: DateInput) -> Opti
126131
return None
127132

128133
columns = ["Date", "aa", "bb", "cc", "bid", "ask", "dd", "ee"]
129-
df = pd.read_csv(StringIO(res.text), delimiter=";", header=None, names=columns, dtype=str)
134+
df = pd.read_csv(
135+
StringIO(res.text), delimiter=";", header=None, names=columns, dtype=str
136+
)
130137
df = df.assign(
131138
Date=lambda x: pd.to_datetime(x["Date"], format="%d%m%Y"),
132139
bid=lambda x: x["bid"].str.replace(",", ".").astype(np.float64),
@@ -141,7 +148,11 @@ def _get_symbol(symbol: str, start_date: DateInput, end_date: DateInput) -> Opti
141148

142149

143150
def get(
144-
symbols: Union[str, List[str]], start: DateInput, end: DateInput, side: str = "ask", groupby: str = "symbol"
151+
symbols: Union[str, List[str]],
152+
start: DateInput,
153+
end: DateInput,
154+
side: str = "ask",
155+
groupby: str = "symbol",
145156
) -> pd.DataFrame:
146157
"""
147158
Retorna um DataFrame pandas com séries temporais com taxas de câmbio.

bcb/odata/api.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ def collect(self) -> pd.DataFrame:
4646
data = pd.DataFrame(raw_data["value"])
4747
if not self._raw:
4848
for col in self._DATE_COLUMN_NAMES:
49-
if self.entity.name in self._DATE_COLUMN_NAMES_BY_ENDPOINT and col in self._DATE_COLUMN_NAMES_BY_ENDPOINT[self.entity.name]:
50-
data[col] = pd.to_datetime(data[col], format=self._DATE_COLUMN_NAMES_BY_ENDPOINT[self.entity.name][col])
49+
if (
50+
self.entity.name in self._DATE_COLUMN_NAMES_BY_ENDPOINT
51+
and col in self._DATE_COLUMN_NAMES_BY_ENDPOINT[self.entity.name]
52+
):
53+
data[col] = pd.to_datetime(
54+
data[col],
55+
format=self._DATE_COLUMN_NAMES_BY_ENDPOINT[self.entity.name][
56+
col
57+
],
58+
)
5159
elif col in data.columns:
5260
data[col] = pd.to_datetime(data[col])
5361
return data
@@ -66,6 +74,7 @@ class Endpoint(metaclass=EndpointMeta):
6674
:py:meth:`bcb.odata.api.BaseODataAPI.get_endpoint` das classes que herdam
6775
:py:class:`bcb.odata.api.BaseODataAPI`.
6876
"""
77+
6978
def __init__(self, entity: Any, url: str) -> None:
7079
"""
7180
Construtor da classe Endpoint.

bcb/odata/framework.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ def __repr__(self) -> str:
9090

9191

9292
class ODataFunctionImport:
93-
def __init__(self, name: str, function: "ODataFunction", entity_set: ODataEntitySet) -> None:
93+
def __init__(
94+
self, name: str, function: "ODataFunction", entity_set: ODataEntitySet
95+
) -> None:
9496
self.name = name
9597
self.entity_set = entity_set
9698
self.function = function
@@ -242,7 +244,9 @@ def __repr__(self) -> str:
242244

243245

244246
class ODataEntity:
245-
def __init__(self, name: str, properties: dict[str, ODataProperty], namespace: str) -> None:
247+
def __init__(
248+
self, name: str, properties: dict[str, ODataProperty], namespace: str
249+
) -> None:
246250
self.name = name
247251
self.properties = properties
248252
self.fullname = f"{namespace}.{name}"
@@ -287,7 +291,9 @@ def _parse_entities(self, schema: Any) -> None:
287291
for e in schema.xpath(_xpath, namespaces=self.namespaces)
288292
]
289293

290-
self._entities_fullnames: dict[str, ODataEntity] = {e.fullname: e for e in self.entities}
294+
self._entities_fullnames: dict[str, ODataEntity] = {
295+
e.fullname: e for e in self.entities
296+
}
291297

292298
def _parse_entity_sets(self, schema: Any) -> None:
293299
_xpath = "edm:EntityContainer/edm:EntitySet"
@@ -354,7 +360,9 @@ def __init__(self, url: str) -> None:
354360
self.url = url
355361
res = httpx.get(self.url, timeout=60.0)
356362
self.api_data: dict[str, Any] = json.loads(res.text)
357-
self.endpoints: list[ODataEndPoint] = [ODataEndPoint(**x) for x in self.api_data["value"]]
363+
self.endpoints: list[ODataEndPoint] = [
364+
ODataEndPoint(**x) for x in self.api_data["value"]
365+
]
358366
self._odata_context_url: str = self.api_data["@odata.context"]
359367
self.metadata = ODataMetadata(self._odata_context_url)
360368

@@ -396,12 +404,16 @@ def describe(self) -> None:
396404
for es in self.function_imports.keys():
397405
print(" ", es)
398406

399-
def query(self, entity_set: Union[ODataEntitySet, ODataFunctionImport]) -> "ODataQuery":
407+
def query(
408+
self, entity_set: Union[ODataEntitySet, ODataFunctionImport]
409+
) -> "ODataQuery":
400410
return ODataQuery(entity_set, self.url)
401411

402412

403413
class ODataQuery:
404-
def __init__(self, entity: Union[ODataEntitySet, ODataFunctionImport], url: str) -> None:
414+
def __init__(
415+
self, entity: Union[ODataEntitySet, ODataFunctionImport], url: str
416+
) -> None:
405417
self.entity = entity
406418
self.base_url = url
407419
self._params: dict[str, Any] = {}

bcb/sgs/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ def _get_url_and_payload(
7171
payload["dataFinal"] = Date(end_date).date.strftime("%d/%m/%Y")
7272
url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.{code}/dados"
7373
else:
74-
url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.{code}/dados/ultimos/{last}"
74+
url = (
75+
f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.{code}/dados/ultimos/{last}"
76+
)
7577

7678
return url, payload
7779

bcb/sgs/regional_economy.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,24 @@
124124
}
125125

126126

127-
def get_non_performing_loans_codes(states_or_region: Union[str, List[str]], mode: str = "total") -> Dict[str, str]:
127+
def get_non_performing_loans_codes(
128+
states_or_region: Union[str, List[str]], mode: str = "total"
129+
) -> Dict[str, str]:
128130
is_state = False
129131
is_region = False
130-
states_or_region = [states_or_region] if isinstance(states_or_region, str) else states_or_region
132+
states_or_region = (
133+
[states_or_region] if isinstance(states_or_region, str) else states_or_region
134+
)
131135
states_or_region = [location.upper() for location in states_or_region]
132-
if any(location in list(NON_PERFORMING_LOANS_BY_STATE_TOTAL.keys()) for location in states_or_region):
136+
if any(
137+
location in list(NON_PERFORMING_LOANS_BY_STATE_TOTAL.keys())
138+
for location in states_or_region
139+
):
133140
is_state = True
134-
elif any(location in list(NON_PERFORMING_LOANS_BY_REGION_TOTAL.keys()) for location in states_or_region):
141+
elif any(
142+
location in list(NON_PERFORMING_LOANS_BY_REGION_TOTAL.keys())
143+
for location in states_or_region
144+
):
135145
is_region = True
136146

137147
if not is_state and not is_region:

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
# Fixtures
7373
# ---------------------------------------------------------------------------
7474

75+
7576
@pytest.fixture(autouse=True)
7677
def clear_currency_cache():
7778
"""Clear module-level currency cache before and after each test."""

0 commit comments

Comments
 (0)