Skip to content

Commit a4b17ca

Browse files
committed
ADD: Client library support for backend warnings
1 parent 6bceba0 commit a4b17ca

File tree

4 files changed

+119
-4
lines changed

4 files changed

+119
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Changed `Live` callbacks to no longer yield DBN metadata
77
- Added `metadata` property to `Live`
88
- Added `DatatbentoLiveProtocol` class
9+
- Added support for emitting warnings in API response headers
910
- Upgraded `aiohttp` to 3.8.3
1011
- Upgraded `numpy` to to 1.23.5
1112
- Upgraded `pandas` to to 1.5.3

databento/common/error.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,8 @@ class BentoWarning(Warning):
103103
"""
104104
Represents a Databento specific warning.
105105
"""
106+
107+
class BentoDeprecationWarning(BentoWarning):
108+
"""
109+
Represents a Databento deprecation warning.
110+
"""

databento/historical/http.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1+
import json
12
import sys
3+
import warnings
24
from io import BufferedIOBase
35
from json.decoder import JSONDecodeError
4-
from typing import Any, List, Optional, Tuple
6+
from typing import Any, List, Optional, Tuple, Union
57

68
import aiohttp
79
import requests
8-
from aiohttp import ClientResponse, ContentTypeError
9-
from databento.common.error import BentoClientError, BentoServerError
10-
from databento.version import __version__
10+
from aiohttp import ClientResponse
11+
from aiohttp import ContentTypeError
1112
from requests import Response
1213
from requests.auth import HTTPBasicAuth
1314

15+
from databento.common.error import BentoClientError
16+
from databento.common.error import BentoDeprecationWarning
17+
from databento.common.error import BentoServerError
18+
from databento.common.error import BentoWarning
19+
from databento.version import __version__
20+
1421

1522
_32KB = 1024 * 32 # 32_768
23+
WARNING_HEADER_FIELD: str = "X-Warning"
1624

1725

1826
class BentoHttpAPI:
@@ -51,6 +59,7 @@ def _get(
5159
auth=HTTPBasicAuth(username=self._key, password="") if basic_auth else None,
5260
timeout=(self.TIMEOUT, self.TIMEOUT),
5361
) as response:
62+
check_backend_warnings(response)
5463
check_http_error(response)
5564
return response
5665

@@ -71,6 +80,7 @@ async def _get_json_async(
7180
else None,
7281
timeout=self.TIMEOUT,
7382
) as response:
83+
check_backend_warnings(response)
7484
await check_http_error_async(response)
7585
return await response.json()
7686

@@ -89,6 +99,7 @@ def _post(
8999
auth=HTTPBasicAuth(username=self._key, password="") if basic_auth else None,
90100
timeout=(self.TIMEOUT, self.TIMEOUT),
91101
) as response:
102+
check_backend_warnings(response)
92103
check_http_error(response)
93104
return response
94105

@@ -109,6 +120,7 @@ def _stream(
109120
timeout=(self.TIMEOUT, self.TIMEOUT),
110121
stream=True,
111122
) as response:
123+
check_backend_warnings(response)
112124
check_http_error(response)
113125

114126
for chunk in response.iter_content(chunk_size=_32KB):
@@ -133,6 +145,7 @@ async def _stream_async(
133145
else None,
134146
timeout=self.TIMEOUT,
135147
) as response:
148+
check_backend_warnings(response)
136149
await check_http_error_async(response)
137150

138151
async for chunk in response.content.iter_chunks():
@@ -147,6 +160,24 @@ def is_500_series_error(status: int) -> bool:
147160
return status // 100 == 5
148161

149162

163+
def check_backend_warnings(response: Union[Response, ClientResponse]) -> None:
164+
if WARNING_HEADER_FIELD not in response.headers: # type: ignore [arg-type]
165+
return
166+
167+
backend_warnings = json.loads(
168+
response.headers[WARNING_HEADER_FIELD], # type: ignore [arg-type]
169+
)
170+
171+
for bw in backend_warnings:
172+
type_, _, message = bw.partition(": ")
173+
if type_ == "DeprecationWarning":
174+
category = BentoDeprecationWarning
175+
else:
176+
category = BentoWarning # type: ignore [assignment]
177+
178+
warnings.warn(message, category=category, stacklevel=4)
179+
180+
150181
def check_http_error(response: Response) -> None:
151182
if is_500_series_error(response.status_code):
152183
try:

tests/test_historical_warnings.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import json
2+
3+
import pytest
4+
from databento.historical.http import check_backend_warnings
5+
from requests import Response
6+
7+
8+
@pytest.mark.parametrize(
9+
"header_field",
10+
[
11+
"X-Warning",
12+
"x-warning",
13+
],
14+
)
15+
@pytest.mark.parametrize(
16+
"category, message, expected_category",
17+
[
18+
pytest.param("Warning", "this is a test", "BentoWarning"),
19+
pytest.param(
20+
"DeprecationWarning",
21+
"you're too old!",
22+
"BentoDeprecationWarning",
23+
),
24+
pytest.param("Warning", "edge: case", "BentoWarning"),
25+
pytest.param("UnknownWarning", "", "BentoWarning"),
26+
],
27+
)
28+
def test_backend_warning(
29+
header_field: str,
30+
category: str,
31+
message: str,
32+
expected_category: str,
33+
) -> None:
34+
"""
35+
Test that a backend warning in a response header is correctly
36+
parsed as a type of BentoWarning.
37+
"""
38+
response = Response()
39+
expected = f'["{category}: {message}"]'
40+
response.headers[header_field] = expected
41+
42+
with pytest.warns() as warnings:
43+
check_backend_warnings(response)
44+
45+
assert len(warnings) == 1
46+
assert warnings.list[0].category.__name__ == expected_category
47+
assert str(warnings.list[0].message) == message
48+
49+
50+
@pytest.mark.parametrize(
51+
"header_field",
52+
[
53+
"X-Warning",
54+
"x-warning",
55+
],
56+
)
57+
def test_multiple_backend_warning(
58+
header_field: str,
59+
) -> None:
60+
"""
61+
Test that multiple backend warnings in a response header are
62+
supported.
63+
"""
64+
response = Response()
65+
backend_warnings = [
66+
"Warning: this is a test",
67+
"DeprecationWarning: you're too old!",
68+
]
69+
response.headers[header_field] = json.dumps(backend_warnings)
70+
71+
with pytest.warns() as warnings:
72+
check_backend_warnings(response)
73+
74+
assert len(warnings) == len(backend_warnings)
75+
assert warnings.list[0].category.__name__ == "BentoWarning"
76+
assert str(warnings.list[0].message) == "this is a test"
77+
assert warnings.list[1].category.__name__ == "BentoDeprecationWarning"
78+
assert str(warnings.list[1].message) == "you're too old!"

0 commit comments

Comments
 (0)