Skip to content

Commit b54a08b

Browse files
feat(client): add configurable timeout support for all API operations
- Add configurable timeouts for single, batch, and list API operations - Default timeouts: 5s (single), 30min (batch), 60s (list API) - Users can override defaults via GeocodioClient constructor - Pass appropriate timeout from each method to _request()
1 parent 31a3abc commit b54a08b

File tree

1 file changed

+29
-9
lines changed

1 file changed

+29
-9
lines changed

src/geocodio/client.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727

2828
class GeocodioClient:
2929
BASE_PATH = "/v1.8" # keep in sync with Geocodio's current version
30+
DEFAULT_SINGLE_TIMEOUT = 5.0
31+
DEFAULT_BATCH_TIMEOUT = 1800.0 # 30 minutes
32+
LIST_API_TIMEOUT = 60.0
3033

3134
@staticmethod
3235
def get_status_exception_mappings() -> Dict[
@@ -43,13 +46,23 @@ def get_status_exception_mappings() -> Dict[
4346
500: GeocodioServerError,
4447
}
4548

46-
def __init__(self, api_key: Optional[str] = None, hostname: str = "api.geocod.io"):
49+
def __init__(
50+
self,
51+
api_key: Optional[str] = None,
52+
hostname: str = "api.geocod.io",
53+
single_timeout: Optional[float] = None,
54+
batch_timeout: Optional[float] = None,
55+
list_timeout: Optional[float] = None,
56+
):
4757
self.api_key: str = api_key or os.getenv("GEOCODIO_API_KEY", "")
4858
if not self.api_key:
4959
raise AuthenticationError(
5060
detail="No API key supplied and GEOCODIO_API_KEY is not set."
5161
)
5262
self.hostname = hostname.rstrip("/")
63+
self.single_timeout = single_timeout or self.DEFAULT_SINGLE_TIMEOUT
64+
self.batch_timeout = batch_timeout or self.DEFAULT_BATCH_TIMEOUT
65+
self.list_timeout = list_timeout or self.LIST_API_TIMEOUT
5366
self._http = httpx.Client(base_url=f"https://{self.hostname}")
5467

5568
# ──────────────────────────────────────────────────────────────────────────
@@ -108,7 +121,8 @@ def geocode(
108121
params["q"] = address
109122
data = None
110123

111-
response = self._request("POST" if data else "GET", endpoint, params, json=data)
124+
timeout = self.batch_timeout if data else self.single_timeout
125+
response = self._request("POST" if data else "GET", endpoint, params, json=data, timeout=timeout)
112126
return self._parse_geocoding_response(response.json())
113127

114128
def reverse(
@@ -138,7 +152,8 @@ def reverse(
138152
params["q"] = coordinate # "lat,lng"
139153
data = None
140154

141-
response = self._request("POST" if data else "GET", endpoint, params, json=data)
155+
timeout = self.batch_timeout if data else self.single_timeout
156+
response = self._request("POST" if data else "GET", endpoint, params, json=data, timeout=timeout)
142157
return self._parse_geocoding_response(response.json())
143158

144159
# ──────────────────────────────────────────────────────────────────────────
@@ -152,13 +167,18 @@ def _request(
152167
params: dict,
153168
json: Optional[dict] = None,
154169
files: Optional[dict] = None,
170+
timeout: Optional[float] = None,
155171
) -> httpx.Response:
156172
logger.debug(f"Making Request: {method} {endpoint}")
157173
logger.debug(f"Params: {params}")
158174
logger.debug(f"JSON body: {json}")
159175
logger.debug(f"Files: {files}")
160176

161-
resp = self._http.request(method, endpoint, params=params, json=json, files=files, timeout=30)
177+
if timeout is None:
178+
timeout = self.single_timeout
179+
180+
logger.debug(f"Using timeout: {timeout}s")
181+
resp = self._http.request(method, endpoint, params=params, json=json, files=files, timeout=timeout)
162182

163183
logger.debug(f"Response status code: {resp.status_code}")
164184
logger.debug(f"Response headers: {resp.headers}")
@@ -284,7 +304,7 @@ def create_list(
284304
# Join fields with commas as required by the API
285305
params["fields"] = ",".join(fields)
286306

287-
response = self._request("POST", endpoint, params, files=files)
307+
response = self._request("POST", endpoint, params, files=files, timeout=self.list_timeout)
288308
logger.debug(f"Response content: {response.text}")
289309
return self._parse_list_response(response.json(), response=response)
290310

@@ -298,7 +318,7 @@ def get_lists(self) -> PaginatedResponse:
298318
params: Dict[str, Union[str, int]] = {"api_key": self.api_key}
299319
endpoint = f"{self.BASE_PATH}/lists"
300320

301-
response = self._request("GET", endpoint, params)
321+
response = self._request("GET", endpoint, params, timeout=self.list_timeout)
302322
pagination_info = response.json()
303323

304324
logger.debug(f"Pagination info: {pagination_info}")
@@ -333,7 +353,7 @@ def get_list(self, list_id: str) -> ListResponse:
333353
params: Dict[str, Union[str, int]] = {"api_key": self.api_key}
334354
endpoint = f"{self.BASE_PATH}/lists/{list_id}"
335355

336-
response = self._request("GET", endpoint, params)
356+
response = self._request("GET", endpoint, params, timeout=self.list_timeout)
337357
return self._parse_list_response(response.json(), response=response)
338358

339359
def delete_list(self, list_id: str) -> None:
@@ -346,7 +366,7 @@ def delete_list(self, list_id: str) -> None:
346366
params: Dict[str, Union[str, int]] = {"api_key": self.api_key}
347367
endpoint = f"{self.BASE_PATH}/lists/{list_id}"
348368

349-
self._request("DELETE", endpoint, params)
369+
self._request("DELETE", endpoint, params, timeout=self.list_timeout)
350370

351371
@staticmethod
352372
def _parse_list_response(response_json: dict, response: httpx.Response = None) -> ListResponse:
@@ -515,7 +535,7 @@ def download(self, list_id: str, filename: Optional[str] = None) -> str | bytes:
515535
params = {"api_key": self.api_key}
516536
endpoint = f"{self.BASE_PATH}/lists/{list_id}/download"
517537

518-
response: httpx.Response = self._request("GET", endpoint, params)
538+
response: httpx.Response = self._request("GET", endpoint, params, timeout=self.list_timeout)
519539
if response.headers.get("content-type", "").startswith("application/json"):
520540
try:
521541
error = response.json()

0 commit comments

Comments
 (0)