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+ # Shared kwargs for every ``httpx`` call site. ``requests`` defaulted to
15+ # following redirects on GET; ``httpx`` does not. ``timeout=None``
16+ # preserves the no-timeout behavior the chunker docs promise.
17+ HTTPX_DEFAULTS = {"follow_redirects" : True , "timeout" : None }
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,29 @@ 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+
257273def query (url , payload , delimiter = "," , ssl_check = True ):
258274 """Send a query.
259275
260- Wrapper for requests .get that handles errors, converts listed
276+ Wrapper for httpx .get that handles errors, converts listed
261277 query parameters to comma separated strings, and returns response.
262278
263279 Parameters
264280 ----------
265281 url: string
266282 URL to query
267283 payload: dict
268- query parameters passed to ``requests .get``
284+ query parameters passed to ``httpx .get``
269285 delimiter: string
270286 delimiter to use with lists
271287 ssl_check: bool
@@ -275,19 +291,32 @@ def query(url, payload, delimiter=",", ssl_check=True):
275291 Returns
276292 -------
277293 string: query response
278- The response from the API query ``requests .get`` function call.
294+ The response from the API query ``httpx .get`` function call.
279295 """
280296
281297 for key , value in payload .items ():
282298 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))
299+ # httpx serializes None params as ``foo=``; USGS rejects with 400.
300+ # Drop them. (``to_str`` returns None for non-iterable scalars like bools.)
301+ payload = { k : v for k , v in payload . items () if v is not None }
286302
287- # define the user agent for the query
288303 user_agent = {"user-agent" : f"python-dataretrieval/{ dataretrieval .__version__ } " }
289304
290- response = requests .get (url , params = payload , headers = user_agent , verify = ssl_check )
305+ try :
306+ response = httpx .get (
307+ url ,
308+ params = payload ,
309+ headers = user_agent ,
310+ verify = ssl_check ,
311+ ** HTTPX_DEFAULTS ,
312+ )
313+ except httpx .InvalidURL as exc :
314+ # httpx rejects oversize URLs client-side; mirror the 414 branch.
315+ raise ValueError (
316+ "Request URL too long. Modify your query to use fewer sites. "
317+ f"httpx rejected the URL client-side: { exc } . Pseudo-code "
318+ f"example of how to split your query: \n { _URL_TOO_LONG_EXAMPLE } "
319+ ) from exc
291320
292321 if response .status_code == 400 :
293322 raise ValueError (
@@ -299,24 +328,14 @@ def query(url, payload, delimiter=",", ssl_check=True):
299328 + f"URL: { response .url } "
300329 )
301330 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"""
312331 raise ValueError (
313332 "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 } "
333+ f"API response reason: { response . reason_phrase } . Pseudo-code "
334+ f"example of how to split your query: \n { _URL_TOO_LONG_EXAMPLE } "
316335 )
317336 elif response .status_code in [500 , 502 , 503 ]:
318337 raise ValueError (
319- f"Service Unavailable: { response .status_code } { response .reason } . "
338+ f"Service Unavailable: { response .status_code } { response .reason_phrase } . "
320339 + f"The service at { response .url } may be down or experiencing issues."
321340 )
322341
0 commit comments