Skip to content

Commit db84d6b

Browse files
wilsonfreitasclaude
andcommitted
Make date column detection configurable in OData API (Phase 7.1)
Add DATE_COLUMNS class variable to BaseODataAPI. When a subclass declares a non-empty DATE_COLUMNS list, EndpointQuery.collect() converts only those columns to datetime instead of running the built-in name-based heuristic. When DATE_COLUMNS is empty (the default), the existing heuristic is unchanged so all current callers keep working without any migration. class MyAPI(BaseODataAPI): DATE_COLUMNS = ["Data", "DataVigencia"] BASE_URL = "..." Wire-up: get_endpoint() passes DATE_COLUMNS to Endpoint, which stores it and forwards it to EndpointQuery via both query() and get(). Add four new tests covering: - empty DATE_COLUMNS → heuristic converts all known date columns - DATE_COLUMNS = ["Data"] → only "Data" is converted; "DataVigencia" stays str - DATE_COLUMNS = ["DataVigencia"] → only "DataVigencia" is converted - propagation through ep.get() shortcut Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5d39321 commit db84d6b

3 files changed

Lines changed: 127 additions & 17 deletions

File tree

bcb/odata/api.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,36 @@ class EndpointQuery(ODataQuery):
4141
"DataVigencia",
4242
}
4343

44+
def __init__(
45+
self,
46+
entity: Any,
47+
url: str,
48+
date_columns: Optional[list[str]] = None,
49+
) -> None:
50+
super().__init__(entity, url)
51+
self._date_columns: list[str] = date_columns or []
52+
4453
def collect(self) -> pd.DataFrame:
4554
raw_data = super().collect()
4655
data = pd.DataFrame(raw_data["value"])
4756
if not self._raw:
48-
for col in self._DATE_COLUMN_NAMES:
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-
)
59-
elif col in data.columns:
60-
data[col] = pd.to_datetime(data[col])
57+
if self._date_columns:
58+
# Use the explicit list provided by the API subclass.
59+
for col in self._date_columns:
60+
if col in data.columns:
61+
data[col] = pd.to_datetime(data[col])
62+
else:
63+
# Fall back to the built-in heuristic.
64+
endpoint_overrides = self._DATE_COLUMN_NAMES_BY_ENDPOINT.get(
65+
self.entity.name, {}
66+
)
67+
for col in self._DATE_COLUMN_NAMES:
68+
if col in endpoint_overrides:
69+
data[col] = pd.to_datetime(
70+
data[col], format=endpoint_overrides[col]
71+
)
72+
elif col in data.columns:
73+
data[col] = pd.to_datetime(data[col])
6174
return data
6275

6376

@@ -75,7 +88,9 @@ class Endpoint(metaclass=EndpointMeta):
7588
:py:class:`bcb.odata.api.BaseODataAPI`.
7689
"""
7790

78-
def __init__(self, entity: Any, url: str) -> None:
91+
def __init__(
92+
self, entity: Any, url: str, date_columns: Optional[list[str]] = None
93+
) -> None:
7994
"""
8095
Construtor da classe Endpoint.
8196
@@ -86,9 +101,13 @@ def __init__(self, entity: Any, url: str) -> None:
86101
Obtidos da classe ``bcb.odata.framework.ODataService``.
87102
url : str
88103
URL da API OData.
104+
date_columns : list[str], optional
105+
Colunas a converter para datetime. Quando fornecido, substitui a
106+
heurística padrão de detecção de datas.
89107
"""
90108
self._entity = entity
91109
self._url = url
110+
self._date_columns: list[str] = date_columns or []
92111

93112
def get(self, *args: Any, **kwargs: Any) -> pd.DataFrame:
94113
"""
@@ -104,7 +123,7 @@ def get(self, *args: Any, **kwargs: Any) -> pd.DataFrame:
104123
-------
105124
pd.DataFrame: resultado da consulta
106125
"""
107-
_query = EndpointQuery(self._entity, self._url)
126+
_query = EndpointQuery(self._entity, self._url, self._date_columns)
108127
for arg in args:
109128
if isinstance(arg, ODataPropertyFilter):
110129
_query.filter(arg)
@@ -138,7 +157,7 @@ def query(self) -> EndpointQuery:
138157
-------
139158
bcb.odata.api.EndpointQuery
140159
"""
141-
return EndpointQuery(self._entity, self._url)
160+
return EndpointQuery(self._entity, self._url, self._date_columns)
142161

143162

144163
class BaseODataAPI:
@@ -149,6 +168,7 @@ class BaseODataAPI:
149168
"""
150169

151170
BASE_URL: str
171+
DATE_COLUMNS: list[str] = []
152172

153173
def __init__(self) -> None:
154174
"""
@@ -198,7 +218,9 @@ def get_endpoint(self, endpoint: str) -> Endpoint:
198218
ValueError
199219
Se o *endpoint* fornecido é errado.
200220
"""
201-
return Endpoint(self.service[endpoint], self.service.url)
221+
return Endpoint(
222+
self.service[endpoint], self.service.url, self.DATE_COLUMNS or None
223+
)
202224

203225

204226
class ODataAPI(BaseODataAPI):

tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@
6767
]
6868
}"""
6969

70+
# Response with two date columns to test DATE_COLUMNS selectivity
71+
ODATA_QUERY_RESPONSE_MULTI_DATE_JSON = """{
72+
"value": [
73+
{"Indicador": "IPCA", "Data": "2021-01-04", "DataVigencia": "2021-06-01", "Mediana": 4.5}
74+
]
75+
}"""
76+
7077

7178
# ---------------------------------------------------------------------------
7279
# Fixtures

tests/test_odata.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ODATA_SERVICE_ROOT_JSON,
1212
ODATA_METADATA_XML,
1313
ODATA_QUERY_RESPONSE_JSON,
14+
ODATA_QUERY_RESPONSE_MULTI_DATE_JSON,
1415
)
1516

1617
EXPECTATIVAS_BASE_URL = (
@@ -142,3 +143,83 @@ def test_endpoint_get_shortcut(httpx_mock):
142143
df = ep.get(limit=1)
143144
assert isinstance(df, pd.DataFrame)
144145
assert len(df) == 1
146+
147+
148+
# ---------------------------------------------------------------------------
149+
# DATE_COLUMNS — configurable date detection (Phase 7.1)
150+
# ---------------------------------------------------------------------------
151+
152+
153+
class _SingleDateAPI(Expectativas):
154+
"""API subclass that restricts date conversion to 'Data' only."""
155+
156+
DATE_COLUMNS = ["Data"]
157+
158+
159+
class _AltDateAPI(Expectativas):
160+
"""API subclass that restricts date conversion to 'DataVigencia' only."""
161+
162+
DATE_COLUMNS = ["DataVigencia"]
163+
164+
165+
def test_date_columns_default_empty_uses_heuristic(httpx_mock):
166+
"""When DATE_COLUMNS is empty (default), the built-in heuristic fires."""
167+
add_service_mocks(httpx_mock)
168+
httpx_mock.add_response(
169+
url=ENTITY_URL_PATTERN,
170+
text=ODATA_QUERY_RESPONSE_MULTI_DATE_JSON,
171+
status_code=200,
172+
)
173+
api = Expectativas() # DATE_COLUMNS = []
174+
ep = api.get_endpoint("ExpectativasMercadoAnuais")
175+
df = ep.query().limit(1).collect()
176+
# Heuristic converts both "Data" and "DataVigencia"
177+
assert isinstance(df["Data"].iloc[0], datetime)
178+
assert isinstance(df["DataVigencia"].iloc[0], datetime)
179+
180+
181+
def test_date_columns_explicit_list_converts_only_listed(httpx_mock):
182+
"""When DATE_COLUMNS lists only 'Data', only that column is converted."""
183+
add_service_mocks(httpx_mock)
184+
httpx_mock.add_response(
185+
url=ENTITY_URL_PATTERN,
186+
text=ODATA_QUERY_RESPONSE_MULTI_DATE_JSON,
187+
status_code=200,
188+
)
189+
api = _SingleDateAPI()
190+
ep = api.get_endpoint("ExpectativasMercadoAnuais")
191+
df = ep.query().limit(1).collect()
192+
assert isinstance(df["Data"].iloc[0], datetime)
193+
# DataVigencia is NOT in DATE_COLUMNS so it stays as a raw string
194+
assert isinstance(df["DataVigencia"].iloc[0], str)
195+
196+
197+
def test_date_columns_explicit_list_alternate_column(httpx_mock):
198+
"""When DATE_COLUMNS lists only 'DataVigencia', only that column is converted."""
199+
add_service_mocks(httpx_mock)
200+
httpx_mock.add_response(
201+
url=ENTITY_URL_PATTERN,
202+
text=ODATA_QUERY_RESPONSE_MULTI_DATE_JSON,
203+
status_code=200,
204+
)
205+
api = _AltDateAPI()
206+
ep = api.get_endpoint("ExpectativasMercadoAnuais")
207+
df = ep.query().limit(1).collect()
208+
assert isinstance(df["DataVigencia"].iloc[0], datetime)
209+
# Data is NOT in DATE_COLUMNS so it stays as a raw string
210+
assert isinstance(df["Data"].iloc[0], str)
211+
212+
213+
def test_date_columns_propagates_through_get_shortcut(httpx_mock):
214+
"""DATE_COLUMNS applies to ep.get() as well as ep.query().collect()."""
215+
add_service_mocks(httpx_mock)
216+
httpx_mock.add_response(
217+
url=ENTITY_URL_PATTERN,
218+
text=ODATA_QUERY_RESPONSE_MULTI_DATE_JSON,
219+
status_code=200,
220+
)
221+
api = _SingleDateAPI()
222+
ep = api.get_endpoint("ExpectativasMercadoAnuais")
223+
df = ep.get(limit=1)
224+
assert isinstance(df["Data"].iloc[0], datetime)
225+
assert isinstance(df["DataVigencia"].iloc[0], str)

0 commit comments

Comments
 (0)