Skip to content

Commit 3bf342e

Browse files
Merge pull request #7 from Geocodio/feat/configurable-timeouts
feat(client): add configurable timeout support for all API operations
2 parents 74d7ee4 + 60e0305 commit 3bf342e

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(
@@ -144,7 +158,8 @@ def reverse(
144158
params["q"] = coordinate # "lat,lng"
145159
data = None
146160

147-
response = self._request("POST" if data else "GET", endpoint, params, json=data)
161+
timeout = self.batch_timeout if data else self.single_timeout
162+
response = self._request("POST" if data else "GET", endpoint, params, json=data, timeout=timeout)
148163
return self._parse_geocoding_response(response.json())
149164

150165
# ──────────────────────────────────────────────────────────────────────────
@@ -158,13 +173,18 @@ def _request(
158173
params: dict,
159174
json: Optional[dict] = None,
160175
files: Optional[dict] = None,
176+
timeout: Optional[float] = None,
161177
) -> httpx.Response:
162178
logger.debug(f"Making Request: {method} {endpoint}")
163179
logger.debug(f"Params: {params}")
164180
logger.debug(f"JSON body: {json}")
165181
logger.debug(f"Files: {files}")
166182

167-
resp = self._http.request(method, endpoint, params=params, json=json, files=files, timeout=60)
183+
if timeout is None:
184+
timeout = self.single_timeout
185+
186+
logger.debug(f"Using timeout: {timeout}s")
187+
resp = self._http.request(method, endpoint, params=params, json=json, files=files, timeout=timeout)
168188

169189
logger.debug(f"Response status code: {resp.status_code}")
170190
logger.debug(f"Response headers: {resp.headers}")
@@ -290,7 +310,7 @@ def create_list(
290310
# Join fields with commas as required by the API
291311
params["fields"] = ",".join(fields)
292312

293-
response = self._request("POST", endpoint, params, files=files)
313+
response = self._request("POST", endpoint, params, files=files, timeout=self.list_timeout)
294314
logger.debug(f"Response content: {response.text}")
295315
return self._parse_list_response(response.json(), response=response)
296316

@@ -304,7 +324,7 @@ def get_lists(self) -> PaginatedResponse:
304324
params: Dict[str, Union[str, int]] = {"api_key": self.api_key}
305325
endpoint = f"{self.BASE_PATH}/lists"
306326

307-
response = self._request("GET", endpoint, params)
327+
response = self._request("GET", endpoint, params, timeout=self.list_timeout)
308328
pagination_info = response.json()
309329

310330
logger.debug(f"Pagination info: {pagination_info}")
@@ -339,7 +359,7 @@ def get_list(self, list_id: str) -> ListResponse:
339359
params: Dict[str, Union[str, int]] = {"api_key": self.api_key}
340360
endpoint = f"{self.BASE_PATH}/lists/{list_id}"
341361

342-
response = self._request("GET", endpoint, params)
362+
response = self._request("GET", endpoint, params, timeout=self.list_timeout)
343363
return self._parse_list_response(response.json(), response=response)
344364

345365
def delete_list(self, list_id: str) -> None:
@@ -352,7 +372,7 @@ def delete_list(self, list_id: str) -> None:
352372
params: Dict[str, Union[str, int]] = {"api_key": self.api_key}
353373
endpoint = f"{self.BASE_PATH}/lists/{list_id}"
354374

355-
self._request("DELETE", endpoint, params)
375+
self._request("DELETE", endpoint, params, timeout=self.list_timeout)
356376

357377
@staticmethod
358378
def _parse_list_response(response_json: dict, response: httpx.Response = None) -> ListResponse:
@@ -521,7 +541,7 @@ def download(self, list_id: str, filename: Optional[str] = None) -> str | bytes:
521541
params = {"api_key": self.api_key}
522542
endpoint = f"{self.BASE_PATH}/lists/{list_id}/download"
523543

524-
response: httpx.Response = self._request("GET", endpoint, params)
544+
response: httpx.Response = self._request("GET", endpoint, params, timeout=self.list_timeout)
525545
if response.headers.get("content-type", "").startswith("application/json"):
526546
try:
527547
error = response.json()

0 commit comments

Comments
 (0)