55import warnings
66from collections .abc import Iterable
77
8+ import httpx
89import pandas as pd
9- import requests
1010
1111import dataretrieval
1212from dataretrieval .codes import tz
1313
14+ HTTPX_DEFAULTS = {
15+ "follow_redirects" : True ,
16+ "timeout" : httpx .Timeout (60.0 , connect = 10.0 ),
17+ }
18+
1419
1520def to_str (listlike , delimiter = "," ):
1621 """Translates list-like objects into strings.
@@ -205,7 +210,7 @@ class BaseMetadata:
205210 Response url
206211 query_time: datetme.timedelta
207212 Response elapsed time
208- header: requests.structures.CaseInsensitiveDict
213+ header: httpx.Headers
209214 Response headers
210215
211216 """
@@ -216,7 +221,7 @@ def __init__(self, response) -> None:
216221 Parameters
217222 ----------
218223 response: Response
219- Response object from requests module
224+ Response object from httpx module
220225
221226 Returns
222227 -------
@@ -225,8 +230,8 @@ def __init__(self, response) -> None:
225230
226231 """
227232
228- # These are built from the API response
229- self .url = response .url
233+ # Coerce httpx.URL -> str: BaseMetadata.url has always been str.
234+ self .url = str ( response .url )
230235 self .query_time = response .elapsed
231236 self .header = response .headers
232237 self .comment = None
@@ -254,18 +259,37 @@ def __repr__(self) -> str:
254259 return f"{ type (self ).__name__ } (url={ self .url } )"
255260
256261
262+ _URL_TOO_LONG_EXAMPLE = """
263+ # n is the number of chunks to divide the query into \n
264+ split_list = np.array_split(site_list, n)
265+ data_list = [] # list to store chunk results in \n
266+ # loop through chunks and make requests \n
267+ for site_list in split_list: \n
268+ data = nwis.get_record(sites=site_list, service='dv', \n
269+ start=start, end=end) \n
270+ data_list.append(data) # append results to list"""
271+
272+
273+ def _url_too_long_error (detail : str ) -> ValueError :
274+ return ValueError (
275+ "Request URL too long. Modify your query to use fewer sites. "
276+ f"{ detail } . Pseudo-code example of how to split your query: "
277+ f"\n { _URL_TOO_LONG_EXAMPLE } "
278+ )
279+
280+
257281def query (url , payload , delimiter = "," , ssl_check = True ):
258282 """Send a query.
259283
260- Wrapper for requests .get that handles errors, converts listed
284+ Wrapper for httpx .get that handles errors, converts listed
261285 query parameters to comma separated strings, and returns response.
262286
263287 Parameters
264288 ----------
265289 url: string
266290 URL to query
267291 payload: dict
268- query parameters passed to ``requests .get``
292+ query parameters passed to ``httpx .get``
269293 delimiter: string
270294 delimiter to use with lists
271295 ssl_check: bool
@@ -275,19 +299,27 @@ def query(url, payload, delimiter=",", ssl_check=True):
275299 Returns
276300 -------
277301 string: query response
278- The response from the API query ``requests .get`` function call.
302+ The response from the API query ``httpx .get`` function call.
279303 """
280304
281305 for key , value in payload .items ():
282306 payload [key ] = to_str (value , delimiter )
283- # for index in range(len(payload)):
284- # key, value = payload[index]
285- # payload[index] = (key, to_str(value))
307+ # httpx serializes None params as ``foo=``; USGS rejects with 400.
308+ # Drop them. (``to_str`` returns None for non-iterable scalars like bools.)
309+ payload = { k : v for k , v in payload . items () if v is not None }
286310
287- # define the user agent for the query
288311 user_agent = {"user-agent" : f"python-dataretrieval/{ dataretrieval .__version__ } " }
289312
290- response = requests .get (url , params = payload , headers = user_agent , verify = ssl_check )
313+ try :
314+ response = httpx .get (
315+ url ,
316+ params = payload ,
317+ headers = user_agent ,
318+ verify = ssl_check ,
319+ ** HTTPX_DEFAULTS ,
320+ )
321+ except httpx .InvalidURL as exc :
322+ raise _url_too_long_error (f"httpx rejected the URL client-side: { exc } " ) from exc
291323
292324 if response .status_code == 400 :
293325 raise ValueError (
@@ -299,24 +331,10 @@ def query(url, payload, delimiter=",", ssl_check=True):
299331 + f"URL: { response .url } "
300332 )
301333 elif response .status_code == 414 :
302- _reason = response .reason
303- _example = """
304- # n is the number of chunks to divide the query into \n
305- split_list = np.array_split(site_list, n)
306- data_list = [] # list to store chunk results in \n
307- # loop through chunks and make requests \n
308- for site_list in split_list: \n
309- data = nwis.get_record(sites=site_list, service='dv', \n
310- start=start, end=end) \n
311- data_list.append(data) # append results to list"""
312- raise ValueError (
313- "Request URL too long. Modify your query to use fewer sites. "
314- + f"API response reason: { _reason } . Pseudo-code example of how to "
315- + f"split your query: \n { _example } "
316- )
317- elif response .status_code in [500 , 502 , 503 ]:
334+ raise _url_too_long_error (f"API response reason: { response .reason_phrase } " )
335+ elif 500 <= response .status_code < 600 :
318336 raise ValueError (
319- f"Service Unavailable: { response .status_code } { response .reason } . "
337+ f"Service Unavailable: { response .status_code } { response .reason_phrase } . "
320338 + f"The service at { response .url } may be down or experiencing issues."
321339 )
322340
0 commit comments