Skip to content

Commit 988f10a

Browse files
rzatsmelange396
andauthored
Add inspection modes to Python client library (#1418)
* Add inspection modes to Python client library * Reset sandbox & debug globals * Update based on reviews * Add auth logging Co-authored-by: george <[email protected]> --------- Co-authored-by: george <[email protected]>
1 parent c8c4830 commit 988f10a

File tree

3 files changed

+103
-0
lines changed

3 files changed

+103
-0
lines changed

dev/local/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ packages =
2424
delphi.epidata.acquisition.twtr
2525
delphi.epidata.acquisition.wiki
2626
delphi.epidata.client
27+
delphi.epidata.common
2728
delphi.epidata.server
2829
delphi.epidata.server.admin
2930
delphi.epidata.server.admin.templates

integrations/client/test_delphi_epidata.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# standard library
44
import time
55
from json import JSONDecodeError
6+
from requests.models import Response
67
from unittest.mock import MagicMock, patch
78

89
# first party
@@ -41,6 +42,8 @@ def localSetUp(self):
4142
# use the local instance of the Epidata API
4243
Epidata.BASE_URL = 'http://delphi_web_epidata/epidata'
4344
Epidata.auth = ('epidata', 'key')
45+
Epidata.debug = False
46+
Epidata.sandbox = False
4447

4548
# use the local instance of the epidata database
4649
secrets.db.host = 'delphi_database_epidata'
@@ -221,6 +224,82 @@ def test_retry_request(self, get):
221224
{'result': 0, 'message': 'error: Expecting value: line 1 column 1 (char 0)'}
222225
)
223226

227+
@patch('requests.post')
228+
@patch('requests.get')
229+
def test_debug(self, get, post):
230+
"""Test that in debug mode request params are correctly logged."""
231+
class MockResponse:
232+
def __init__(self, content, status_code):
233+
self.content = content
234+
self.status_code = status_code
235+
def raise_for_status(self): pass
236+
237+
Epidata.debug = True
238+
239+
try:
240+
with self.subTest(name='test multiple GET'):
241+
with self.assertLogs('delphi_epidata_client', level='INFO') as logs:
242+
get.reset_mock()
243+
get.return_value = MockResponse(b'{"key": "value"}', 200)
244+
Epidata._request_with_retry("test_endpoint1", params={"key1": "value1"})
245+
Epidata._request_with_retry("test_endpoint2", params={"key2": "value2"})
246+
247+
output = logs.output
248+
self.assertEqual(len(output), 4) # [request, response, request, response]
249+
self.assertIn("Sending GET request", output[0])
250+
self.assertIn("\"url\": \"http://delphi_web_epidata/epidata/test_endpoint1/\"", output[0])
251+
self.assertIn("\"params\": {\"key1\": \"value1\"}", output[0])
252+
self.assertIn("Received response", output[1])
253+
self.assertIn("\"status_code\": 200", output[1])
254+
self.assertIn("\"len\": 16", output[1])
255+
self.assertIn("Sending GET request", output[2])
256+
self.assertIn("\"url\": \"http://delphi_web_epidata/epidata/test_endpoint2/\"", output[2])
257+
self.assertIn("\"params\": {\"key2\": \"value2\"}", output[2])
258+
self.assertIn("Received response", output[3])
259+
self.assertIn("\"status_code\": 200", output[3])
260+
self.assertIn("\"len\": 16", output[3])
261+
262+
with self.subTest(name='test GET and POST'):
263+
with self.assertLogs('delphi_epidata_client', level='INFO') as logs:
264+
get.reset_mock()
265+
get.return_value = MockResponse(b'{"key": "value"}', 414)
266+
post.reset_mock()
267+
post.return_value = MockResponse(b'{"key": "value"}', 200)
268+
Epidata._request_with_retry("test_endpoint3", params={"key3": "value3"})
269+
270+
output = logs.output
271+
self.assertEqual(len(output), 3) # [request, response, request, response]
272+
self.assertIn("Sending GET request", output[0])
273+
self.assertIn("\"url\": \"http://delphi_web_epidata/epidata/test_endpoint3/\"", output[0])
274+
self.assertIn("\"params\": {\"key3\": \"value3\"}", output[0])
275+
self.assertIn("Received 414 response, retrying as POST request", output[1])
276+
self.assertIn("\"url\": \"http://delphi_web_epidata/epidata/test_endpoint3/\"", output[1])
277+
self.assertIn("\"params\": {\"key3\": \"value3\"}", output[1])
278+
self.assertIn("Received response", output[2])
279+
self.assertIn("\"status_code\": 200", output[2])
280+
self.assertIn("\"len\": 16", output[2])
281+
finally: # make sure this global is always reset
282+
Epidata.debug = False
283+
284+
@patch('requests.post')
285+
@patch('requests.get')
286+
def test_sandbox(self, get, post):
287+
"""Test that in debug + sandbox mode request params are correctly logged, but no queries are sent."""
288+
Epidata.debug = True
289+
Epidata.sandbox = True
290+
try:
291+
with self.assertLogs('delphi_epidata_client', level='INFO') as logs:
292+
Epidata.covidcast('src', 'sig', 'day', 'county', 20200414, '01234')
293+
output = logs.output
294+
self.assertEqual(len(output), 1)
295+
self.assertIn("Sending GET request", output[0])
296+
self.assertIn("\"url\": \"http://delphi_web_epidata/epidata/covidcast/\"", output[0])
297+
get.assert_not_called()
298+
post.assert_not_called()
299+
finally: # make sure these globals are always reset
300+
Epidata.debug = False
301+
Epidata.sandbox = False
302+
224303
def test_geo_value(self):
225304
"""test different variants of geo types: single, *, multi."""
226305

src/client/delphi_epidata.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010

1111
# External modules
1212
import requests
13+
import time
1314
import asyncio
1415
from tenacity import retry, stop_after_attempt
1516

1617
from aiohttp import ClientSession, TCPConnector, BasicAuth
1718
from importlib.metadata import version, PackageNotFoundError
1819

20+
from delphi.epidata.common.logger import get_structured_logger
21+
1922
# Obtain package version for the user-agent. Uses the installed version by
2023
# preference, even if you've installed it and then use this script independently
2124
# by accident.
@@ -49,6 +52,10 @@ class Epidata:
4952

5053
client_version = _version
5154

55+
logger = get_structured_logger('delphi_epidata_client')
56+
debug = False # if True, prints extra logging statements
57+
sandbox = False # if True, will not execute any queries
58+
5259
# Helper function to cast values and/or ranges to strings
5360
@staticmethod
5461
def _listitem(value):
@@ -71,9 +78,25 @@ def _list(values):
7178
def _request_with_retry(endpoint, params={}):
7279
"""Make request with a retry if an exception is thrown."""
7380
request_url = f"{Epidata.BASE_URL}/{endpoint}/"
81+
if Epidata.debug:
82+
Epidata.logger.info("Sending GET request", url=request_url, params=params, headers=_HEADERS, auth=Epidata.auth)
83+
if Epidata.sandbox:
84+
resp = requests.Response()
85+
resp._content = b'true'
86+
return resp
87+
start_time = time.time()
7488
req = requests.get(request_url, params, auth=Epidata.auth, headers=_HEADERS)
7589
if req.status_code == 414:
90+
if Epidata.debug:
91+
Epidata.logger.info("Received 414 response, retrying as POST request", url=request_url, params=params, headers=_HEADERS)
7692
req = requests.post(request_url, params, auth=Epidata.auth, headers=_HEADERS)
93+
if Epidata.debug:
94+
Epidata.logger.info(
95+
"Received response",
96+
status_code=req.status_code,
97+
len=len(req.content),
98+
time=round(time.time() - start_time, 4)
99+
)
77100
# handle 401 and 429
78101
req.raise_for_status()
79102
return req

0 commit comments

Comments
 (0)