Skip to content

Commit 928b940

Browse files
wilsonfreitasclaude
andcommitted
Phase 8: Logging & Documentation - Complete
Logging Implementation: - Add logging.getLogger(__name__) to all core modules (currency, sgs, odata/framework) - Log HTTP requests with debug level: URL, params (no secrets), status, response length - Log retry attempts with warning level for connection errors - Log date rollback attempts in currency module Example Scripts: - examples/sgs_time_series.py - Basic SGS usage, multiple series, text output - examples/currency_exchange.py - USD/EUR rates, bid/ask filtering, cache management - examples/odata_query.py - OData filtering, sorting, column selection with examples - examples/async_usage.py - Async API usage, concurrent operations with asyncio Documentation Updates: - Add "Which Module Should I Use?" decision table to README - Guides users to right module based on use case - Compares SGS vs PTAX currency, OData services - Shows async API option - Add "Quick Start" section with code examples for each module - Add comprehensive FAQ covering: - Module differences and data coverage - Historical data ranges - Async API usage - Error handling and specific exceptions - Logging setup for debugging - Caching behavior and management - Long-running application considerations - Contributing and documentation resources Definition of Done: - ✅ 103 unit tests pass (67 original + 12 currency negative + 15 SGS negative + 9 async) - ✅ ruff check: All checks passed - ✅ ruff format: All files properly formatted - ✅ mypy: No type errors (strict mode) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 663ef00 commit 928b940

8 files changed

Lines changed: 464 additions & 1 deletion

File tree

README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,134 @@ macroeconômicas fornecidos por um conjuto de instituições do mercado
5959
financeiro.
6060
A classe `bcb.Expectativas` implementa essa interface no
6161
padrão OData.
62+
63+
# Which Module Should I Use?
64+
65+
Use this table to choose the right module for your use case:
66+
67+
| Use Case | Module | Key Features |
68+
|----------|--------|--------------|
69+
| Daily time series (inflation, interest rates) | `bcb.sgs` | Largest historical dataset, granular frequency control, multiple pre-defined series |
70+
| Daily foreign exchange rates (PTAX) | `bcb.currency` | Bid/ask spreads, quick implementation, daily frequency |
71+
| Market expectations (FOCUS survey) | `bcb.odata` (Expectativas) | Forward-looking economic indicators, consensus forecasts |
72+
| Interest rates (various types) | `bcb.odata` (TaxaJuros) | Detailed rate curves, real estate lending rates |
73+
| Real estate financing data | `bcb.odata` (MercadoImobiliario) | Mortgage originations, average rates, volumes |
74+
| Financial institution information | `bcb.odata` (IFDATA) | Bank balance sheet data, regulatory information |
75+
| Advanced data analysis with filters | `bcb.odata` (any service) | Chainable API, SQL-like filtering, sorting, selection |
76+
| Concurrent data fetching | Any module with `async_get()` | Non-blocking requests, improved performance for bulk operations |
77+
78+
# Quick Start
79+
80+
## Time Series with SGS
81+
82+
```python
83+
from bcb import sgs
84+
85+
# Fetch SELIC rate (code 1)
86+
df = sgs.get(1, start="2023-01-01", end="2024-12-31")
87+
```
88+
89+
## Exchange Rates with Currency
90+
91+
```python
92+
from bcb import currency
93+
94+
# Fetch USD bid/ask prices
95+
usd = currency.get("USD", start="2023-01-01", end="2024-12-31")
96+
```
97+
98+
## Market Expectations with OData
99+
100+
```python
101+
from bcb import Expectativas
102+
103+
api = Expectativas()
104+
endpoint = api.get_endpoint("ExpectativasMercadoAnuais")
105+
106+
# Get IPCA forecasts
107+
df = endpoint.query().filter(endpoint.Indicador == "IPCA").limit(100).collect()
108+
```
109+
110+
# FAQ
111+
112+
## Q: What's the difference between SGS and PTAX currency data?
113+
**A:** SGS contains mostly economic indicators. For currency exchange rates, use `bcb.currency` (PTAX data) for daily rates or `bcb.odata` PTAX service for detailed institutional data. The currency module is simpler for common use cases.
114+
115+
## Q: How far back does historical data go?
116+
**A:** It varies by series:
117+
- SGS: Most series go back to 1980s or 1990s (check specific code documentation)
118+
- Currency: Daily rates available from approximately 1980
119+
- OData services: Varies; check BCB documentation for specific endpoints
120+
121+
## Q: Can I fetch data asynchronously?
122+
**A:** Yes! All modules have `async_get()` or similar async methods. Use them for concurrent requests:
123+
```python
124+
import asyncio
125+
from bcb import sgs
126+
127+
async def main():
128+
results = await asyncio.gather(
129+
sgs.async_get(1), # SELIC
130+
sgs.async_get(433), # IPCA
131+
)
132+
133+
asyncio.run(main())
134+
```
135+
136+
## Q: How do I handle errors/missing data?
137+
**A:** The library raises specific exceptions:
138+
- `CurrencyNotFoundError`: Currency symbol not found
139+
- `SGSError`: SGS service error
140+
- `BCBRateLimitError`: Rate limit exceeded (HTTP 429)
141+
- `BCBAPIError`: Other API errors
142+
143+
```python
144+
from bcb import sgs
145+
from bcb.exceptions import SGSError, BCBRateLimitError
146+
147+
try:
148+
df = sgs.get(99999) # Invalid code
149+
except SGSError as e:
150+
print(f"Data error: {e}")
151+
except BCBRateLimitError:
152+
print("Rate limited - please try again later")
153+
```
154+
155+
## Q: How do I enable logging to debug requests?
156+
**A:** The library uses Python's standard logging module:
157+
```python
158+
import logging
159+
160+
logging.basicConfig(level=logging.DEBUG)
161+
logger = logging.getLogger("bcb")
162+
logger.setLevel(logging.DEBUG)
163+
164+
# Now all HTTP requests/responses will be logged
165+
```
166+
167+
## Q: Is there caching to avoid redundant requests?
168+
**A:** Yes:
169+
- `bcb.currency`: Automatic in-memory cache of currency lists
170+
- `bcb.odata`: OData metadata cached per service URL
171+
- Call `currency.clear_cache()` to reset if data changes
172+
173+
## Q: Can I use this in a long-running application?
174+
**A:** Yes, but be mindful of:
175+
- Rate limits: BCB APIs may have limits; implement backoff if needed
176+
- Caching: Currency cache persists in memory; clear it if data updates matter
177+
- Connection pooling: Uses httpx with connection pooling by default
178+
- Async API: Use async methods for truly non-blocking behavior
179+
180+
## Q: How do I contribute or report issues?
181+
**A:** Visit the [GitHub repository](https://github.com/wilsonfreitas/python-bcb) to:
182+
- Report bugs
183+
- Request features
184+
- Submit pull requests
185+
- View documentation
186+
187+
## Q: Where can I find more detailed documentation?
188+
**A:**
189+
- [API Documentation](https://bcb-python.readthedocs.io/)
190+
- [Examples Directory](./examples/)
191+
- Inline docstrings: `help(bcb.sgs.get)`, `help(bcb.currency.get)`, etc.
192+
- [BCB Open Data Portal](https://dadosabertos.bcb.gov.br/)

bcb/currency.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import logging
45
import re
56
import threading
67
from datetime import date, timedelta
@@ -24,6 +25,8 @@
2425
if TYPE_CHECKING:
2526
import httpx
2627

28+
logger = logging.getLogger(__name__)
29+
2730
"""
2831
O módulo :py:mod:`bcb.currency` tem como objetivo fazer consultas no site do conversor de moedas do BCB.
2932
"""
@@ -161,7 +164,11 @@ def _currency_id_list(
161164
"https://ptax.bcb.gov.br/ptax_internet/consultaBoletim.do?"
162165
"method=exibeFormularioConsultaBoletim"
163166
)
167+
logger.debug(f"Fetching currency ID list from {url1}")
164168
res = _CLIENT.get(url1)
169+
logger.debug(
170+
f"Currency ID list response: status={res.status_code}, length={len(res.content)}"
171+
)
165172
if res.status_code == 429:
166173
raise BCBRateLimitError(
167174
"BCB API rate limit exceeded. Please try again later.",
@@ -228,18 +235,28 @@ def _get_valid_currency_list(
228235
)
229236

230237
url2 = f"https://www4.bcb.gov.br/Download/fechamento/M{_date:%Y%m%d}.csv"
238+
logger.debug(f"Fetching currency list from {url2}")
231239
try:
232240
res = _CLIENT.get(url2)
233241
except Exception as ex:
234242
# Connection error: retry same date up to 3 times
235243
if n >= 3:
236244
raise ex
245+
logger.warning(
246+
f"Connection error fetching {url2}, retrying (attempt {n + 1}/3)"
247+
)
237248
return _get_valid_currency_list(_date, n + 1, max_rollback)
238249

250+
logger.debug(
251+
f"Currency list response: status={res.status_code}, length={len(res.content)}"
252+
)
239253
if res.status_code == 200:
240254
return res
241255
else:
242256
# Non-200 response (file not found for date): roll back to previous day
257+
logger.debug(
258+
f"Currency list not found for {_date}, rolling back to previous day"
259+
)
243260
return _get_valid_currency_list(_date - timedelta(1), 0, max_rollback)
244261

245262

@@ -329,7 +346,11 @@ def _fetch_symbol_response(
329346
"""
330347
cid = _get_currency_id(symbol) # Raises CurrencyNotFoundError if not found
331348
url = _currency_url(cid, start_date, end_date)
349+
logger.debug(f"Fetching currency data for {symbol} from {url.split('?')[0]}")
332350
res = _CLIENT.get(url)
351+
logger.debug(
352+
f"Currency data response: status={res.status_code}, length={len(res.content)}"
353+
)
333354

334355
# Handle HTML error response (e.g., no data for date range)
335356
if res.headers["Content-Type"].startswith("text/html"):

bcb/odata/framework.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
import threading
45
from io import BytesIO
56
from typing import Any, Optional, Union
@@ -12,6 +13,8 @@
1213
from bcb.http import _CLIENT, _ASYNC_CLIENT
1314
from bcb.exceptions import ODataError
1415

16+
logger = logging.getLogger(__name__)
17+
1518
# Module-level metadata cache for OData services
1619
# Maps service URL → ODataMetadata instance
1720
_METADATA_CACHE: dict[str, "ODataMetadata"] = {}
@@ -279,7 +282,11 @@ def __init__(self, url: str) -> None:
279282
self._parse_function_imports(schema)
280283

281284
def _load_document(self) -> None:
285+
logger.debug(f"Fetching OData metadata from {self.url}")
282286
res = _CLIENT.get(self.url)
287+
logger.debug(
288+
f"OData metadata response: status={res.status_code}, length={len(res.content)}"
289+
)
283290
self.doc = etree.parse(BytesIO(res.content))
284291

285292
def _parse_entity(self, entity_element: Any, namespace: str) -> ODataEntity:
@@ -551,7 +558,12 @@ def text(self) -> str:
551558
params["@" + (p.name or "")] = p.format(val)
552559
qs = "&".join([f"{quote(k)}={quote(str(v))}" for k, v in params.items()])
553560
headers = {"OData-Version": "4.0", "OData-MaxVersion": "4.0"}
554-
res = _CLIENT.get(self.odata_url() + "?" + qs, headers=headers)
561+
url = self.odata_url()
562+
logger.debug(f"Fetching OData query from {url}")
563+
res = _CLIENT.get(url + "?" + qs, headers=headers)
564+
logger.debug(
565+
f"OData query response: status={res.status_code}, length={len(res.text)}"
566+
)
555567
return res.text
556568

557569
def show(self) -> None:

bcb/sgs/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import asyncio
44
import json
5+
import logging
56
from dataclasses import dataclass
67
from io import StringIO
78
from typing import (
@@ -23,6 +24,8 @@
2324
from bcb.exceptions import BCBRateLimitError, SGSError
2425
from bcb.utils import Date, DateInput
2526

27+
logger = logging.getLogger(__name__)
28+
2629
"""
2730
Sistema Gerenciador de Séries Temporais (SGS)
2831
@@ -332,7 +335,9 @@ def get_json(
332335
série temporal univariada em formato JSON.
333336
"""
334337
url, payload = _get_url_and_payload(code, start, end, last)
338+
logger.debug(f"Fetching SGS time series code={code} from {url.split('/dados')[0]}")
335339
res = _CLIENT.get(url, params=payload)
340+
logger.debug(f"SGS response: status={res.status_code}, length={len(res.text)}")
336341

337342
# Check for rate limiting first
338343
if res.status_code == 429:
@@ -388,7 +393,13 @@ async def async_get_json(
388393
Se a API retorna um erro
389394
"""
390395
url, payload = _get_url_and_payload(code, start, end, last)
396+
logger.debug(
397+
f"Fetching SGS time series (async) code={code} from {url.split('/dados')[0]}"
398+
)
391399
res = await _ASYNC_CLIENT.get(url, params=payload)
400+
logger.debug(
401+
f"SGS (async) response: status={res.status_code}, length={len(res.text)}"
402+
)
392403

393404
# Check for rate limiting first
394405
if res.status_code == 429:

examples/async_usage.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Example: Using Async APIs
3+
4+
This example demonstrates how to use the async versions of the APIs
5+
for concurrent data fetching (useful for fetching multiple series).
6+
"""
7+
8+
import asyncio
9+
from datetime import datetime
10+
from bcb import sgs, currency
11+
from bcb.odata.api import Expectativas
12+
13+
14+
async def fetch_multiple_sgs_series():
15+
"""Fetch multiple SGS time series concurrently."""
16+
print("Example 1: Fetching multiple SGS series concurrently")
17+
18+
# Fetch SELIC, CDI, and IPCA concurrently
19+
codes = [1, 12, 433] # SELIC, CDI, IPCA
20+
21+
df = await sgs.async_get(codes, start="2023-01-01", end="2024-12-31", multi=True)
22+
print("Concurrent SGS fetch completed")
23+
print(df.head())
24+
print()
25+
26+
27+
async def fetch_multiple_currencies():
28+
"""Fetch currency rates concurrently."""
29+
print("Example 2: Fetching currency rates concurrently")
30+
31+
# Fetch USD rate (note: you'd need to implement multi-symbol async
32+
# for this to truly be concurrent for different symbols)
33+
df = await currency.async_get("USD", start="2024-01-01", end="2024-12-31")
34+
print("Async currency fetch completed")
35+
print(df.head())
36+
print()
37+
38+
39+
async def fetch_odata_async():
40+
"""Fetch OData results asynchronously."""
41+
print("Example 3: Fetching OData results asynchronously")
42+
43+
api = Expectativas()
44+
endpoint = api.get_endpoint("ExpectativasMercadoAnuais")
45+
46+
# Build and execute query asynchronously
47+
query = endpoint.query().filter(endpoint.Indicador == "IPCA").limit(5)
48+
df = await query.async_collect()
49+
print("Async OData fetch completed")
50+
print(df)
51+
print()
52+
53+
54+
async def concurrent_operations():
55+
"""Execute multiple async operations concurrently."""
56+
print("Example 4: Multiple concurrent operations")
57+
58+
# Create tasks for concurrent execution
59+
tasks = [
60+
sgs.async_get(1, start="2024-01-01", end="2024-12-31"), # SELIC
61+
sgs.async_get(11, start="2024-01-01", end="2024-12-31"), # CDI
62+
sgs.async_get(433, start="2024-01-01", end="2024-12-31"), # IPCA
63+
]
64+
65+
# Wait for all tasks to complete
66+
results = await asyncio.gather(*tasks)
67+
print(f"Fetched {len(results)} concurrent series")
68+
print("First series sample:")
69+
print(results[0].head())
70+
print()
71+
72+
73+
async def main():
74+
"""Run all async examples."""
75+
try:
76+
await fetch_multiple_sgs_series()
77+
await fetch_multiple_currencies()
78+
await fetch_odata_async()
79+
await concurrent_operations()
80+
except Exception as e:
81+
print(f"Error: {type(e).__name__}: {e}")
82+
83+
84+
if __name__ == "__main__":
85+
# Run the async examples
86+
# Note: This requires Python 3.7+ with asyncio
87+
asyncio.run(main())

0 commit comments

Comments
 (0)