aio_overpass.error
Error types.
(ClientError)
╷
┌──────────────┼────────────┬────────────────┬────────────────┐
╵ ╵ ╵ ╵ ╵
RunnerError (QueryError) GiveupError ResponseError CallError
╷ ╷ ╷
┌────────────────────────┼──────────────────────┐ │ │
╵ ╵ ╵ ╵ ╵
QueryLanguageError QueryRejectError QueryResponseError CallTimeoutError
1""" 2Error types. 3 4``` 5 (ClientError) 6 ╷ 7 ┌──────────────┼────────────┬────────────────┬────────────────┐ 8 ╵ ╵ ╵ ╵ ╵ 9 RunnerError (QueryError) GiveupError ResponseError CallError 10 ╷ ╷ ╷ 11 ┌────────────────────────┼──────────────────────┐ │ │ 12 ╵ ╵ ╵ ╵ ╵ 13QueryLanguageError QueryRejectError QueryResponseError CallTimeoutError 14``` 15""" 16 17import asyncio 18import html 19import logging 20import math 21import re 22from dataclasses import dataclass 23from enum import Enum, auto 24from json import JSONDecodeError 25from typing import NoReturn, TypeAlias, TypeGuard 26 27import aiohttp 28import aiohttp.typedefs 29 30 31__docformat__ = "google" 32__all__ = ( 33 "AlreadyRunningError", 34 "ClientError", 35 "CallError", 36 "CallTimeoutError", 37 "ResponseError", 38 "ResponseErrorCause", 39 "GiveupError", 40 "GiveupCause", 41 "QueryError", 42 "QueryLanguageError", 43 "QueryRejectError", 44 "QueryRejectCause", 45 "QueryResponseError", 46 "RunnerError", 47 "is_call_err", 48 "is_call_timeout", 49 "is_exceeding_maxsize", 50 "is_exceeding_timeout", 51 "is_gateway_rejection", 52 "is_giveup_err", 53 "is_rejection", 54 "is_runtime_rejection", 55 "is_server_error", 56 "is_too_busy", 57 "is_too_many_queries", 58) 59 60 61@dataclass(kw_only=True) 62class AlreadyRunningError(ValueError): 63 """ 64 Raised when trying to run a query while it's already being run. 65 66 Attributes: 67 kwargs: the query's ``kwargs`` 68 """ 69 70 kwargs: dict 71 72 def __str__(self) -> str: 73 return f"query{self.kwargs!r} is already being run" 74 75 76class ClientError(Exception): 77 """Base exception for failed Overpass API requests and queries.""" 78 79 @property 80 def should_retry(self) -> bool: 81 """Returns ``True`` if it's worth retrying when encountering this error.""" 82 return False 83 84 85@dataclass(kw_only=True) 86class RunnerError(ClientError): 87 """ 88 The query runner raised an exception. 89 90 This is an unexpected error, in contrast to the runner intentionally 91 raising ``query.error`` to stop retrying. The cause of this error 92 is therefore anything but a ``ClientError``. 93 94 Attributes: 95 cause: the exception that caused this error 96 """ 97 98 cause: BaseException 99 100 @property 101 def should_retry(self) -> bool: 102 """Returns ``True`` if it's worth retrying when encountering this error.""" 103 return False 104 105 def __str__(self) -> str: 106 return str(self.cause) 107 108 109@dataclass(kw_only=True) 110class CallError(ClientError): 111 """ 112 Failed to make an API request. 113 114 This error is raised when the client failed to get any response, 115 f.e. due to connection issues. 116 117 Attributes: 118 cause: the exception that caused this error 119 """ 120 121 cause: aiohttp.ClientError | asyncio.TimeoutError 122 123 @property 124 def should_retry(self) -> bool: 125 """Returns ``True`` if it's worth retrying when encountering this error.""" 126 return True 127 128 def __str__(self) -> str: 129 return str(self.cause) 130 131 132@dataclass(kw_only=True) 133class CallTimeoutError(CallError): 134 """ 135 An API request timed out. 136 137 Attributes: 138 cause: the exception that caused this error 139 after_secs: the configured timeout for the request 140 """ 141 142 cause: asyncio.TimeoutError 143 after_secs: float 144 145 @property 146 def should_retry(self) -> bool: 147 """Returns ``True`` if it's worth retrying when encountering this error.""" 148 return True 149 150 def __str__(self) -> str: 151 return str(self.cause) 152 153 154ResponseErrorCause: TypeAlias = aiohttp.ClientResponseError | JSONDecodeError | ValueError 155"""Causes for a ``ResponseError``.""" 156 157 158@dataclass(kw_only=True) 159class ResponseError(ClientError): 160 """ 161 Unexpected API response. 162 163 On the one hand, this can be an error that happened on the Overpass API instance, 164 which is usually signalled by a status code ``>= 500``. In rare cases, 165 the ``cause`` being a ``JSONDecodeError`` also signals this, since it can happen 166 that the API returns a cutoff JSON response. In both of these cases, 167 ``is_server_error()`` will return ``True``. 168 169 On the other hand, this error may indicate that a request failed, but we can't 170 specifically say why. This could be a bug on our end, since the client 171 is meant to process almost any response of an Overpass API instance. 172 Here, ``is_server_error()`` will return ``False``, and the default query runner 173 will log the response body. 174 175 Attributes: 176 response: the unexpected response 177 body: the response body 178 cause: an optional exception that may have caused this error 179 """ 180 181 response: aiohttp.ClientResponse 182 body: str 183 cause: ResponseErrorCause | None 184 185 @property 186 def should_retry(self) -> bool: 187 """Returns ``True`` if it's worth retrying when encountering this error.""" 188 return True 189 190 @property 191 def is_server_error(self) -> bool: 192 """Returns ``True`` if this presumably a server-side error.""" 193 # see class doc for details 194 return self.response.status >= 500 or isinstance(self.cause, JSONDecodeError) 195 196 def __str__(self) -> str: 197 if self.cause is None: 198 return f"unexpected response ({self.response.status})" 199 return f"unexpected response ({self.response.status}): {self.cause}" 200 201 202class GiveupCause(Enum): 203 """Details on why a query was given up on.""" 204 205 RUN_TIMEOUT_BEFORE_STATUS_CALL = auto() 206 """Query's ``run_timeout_secs`` elapsed before a status call before the actual query call.""" 207 208 RUN_TIMEOUT_BY_COOLDOWN = auto() 209 """Query received a timeout that would exceed its remaining ``run_timeout_secs``.""" 210 211 RUN_TIMEOUT_BEFORE_QUERY_CALL = auto() 212 """Query's ``run_timeout_secs`` elapsed before the query call.""" 213 214 EXPECTING_QUERY_TIMEOUT = auto() 215 """ 216 The query's limited [timeout:*] setting is likely too low. 217 218 A query's [timeout:*] setting might be lowered by the client to the remainder of 219 ``run_timeout_secs``. If that happens, and that duration is also lower than a duration 220 after which it was cancelled at the server previously, then this is the cause to give 221 up on the query. 222 223 See Also: 224 ``QueryRejectError.timed_out_after_secs`` 225 """ 226 227 RUN_TIMEOUT_DURING_QUERY_CALL = auto() 228 """Query's ``run_timeout_secs`` elapsed while making the query call, before any response.""" 229 230 231@dataclass(kw_only=True) 232class GiveupError(ClientError): 233 """ 234 The client spent or would spend too long running a query, and gave up. 235 236 This error is raised when the run timeout duration set by a query runner 237 is or would be exceeded. 238 239 Attributes: 240 kwargs: the query's ``kwargs`` 241 after_secs: the total time spent on the query 242 cause: why the query was given up on 243 """ 244 245 kwargs: dict 246 after_secs: float 247 cause: GiveupCause 248 249 @property 250 def should_retry(self) -> bool: 251 """Returns ``True`` if it's worth retrying when encountering this error.""" 252 return False 253 254 def __str__(self) -> str: 255 return f"gave up on query{self.kwargs!r} after {self.after_secs:.01f} seconds" 256 257 258@dataclass(kw_only=True) 259class QueryError(ClientError): 260 """ 261 Base exception for queries that failed at the Overpass API server. 262 263 Attributes: 264 kwargs: the query's ``kwargs`` 265 remarks: the error remarks provided by the API 266 """ 267 268 kwargs: dict 269 remarks: list[str] 270 271 @property 272 def should_retry(self) -> bool: 273 """Returns ``True`` if it's worth retrying when encountering this error.""" 274 return False 275 276 def __str__(self) -> str: 277 first = f"'{self.remarks[0]}'" 278 rest = f" (+{len(self.remarks) - 1} more)" if len(self.remarks) > 1 else "" 279 return f"query{self.kwargs!r} failed: {first}{rest}" 280 281 282@dataclass(kw_only=True) 283class QueryResponseError(ResponseError, QueryError): 284 """ 285 Unexpected query response. 286 287 This error is raised when a query fails (thus extends ``QueryError``), 288 but we can't specifically say why (thus also extends ``ResponseError``). 289 """ 290 291 @property 292 def should_retry(self) -> bool: 293 """Returns ``True`` if it's worth retrying when encountering this error.""" 294 return ResponseError.should_retry.fget(self) # type: ignore[attr-defined] 295 296 def __str__(self) -> str: 297 if self.remarks: 298 first = f"'{self.remarks[0]}'" 299 rest = f" (+{len(self.remarks) - 1} more)" if len(self.remarks) > 1 else "" 300 return f"query{self.kwargs!r} failed: {first}{rest}" 301 302 return f"query{self.kwargs!r} failed with status {self.response.status}" 303 304 305@dataclass(kw_only=True) 306class QueryLanguageError(QueryError): 307 """ 308 Indicates the query's QL code is not valid. 309 310 Retrying is pointless when encountering this error. 311 """ 312 313 @property 314 def should_retry(self) -> bool: 315 """Returns ``True`` if it's worth retrying when encountering this error.""" 316 return False 317 318 319class QueryRejectCause(Enum): 320 """Details why a query was rejected or cancelled by an API server.""" 321 322 TOO_BUSY = auto() 323 """ 324 Gateway rejection. The server has already so much load that the request cannot be executed. 325 326 Smaller ``[timeout:*]`` and/or ``[maxsize:*]`` values might make the request acceptable. 327 """ 328 329 TOO_MANY_QUERIES = auto() 330 """ 331 Gateway rejection. There are no open slots for queries from your IP address. 332 333 Running queries take up a slot, and the number of slots is limited. A client 334 will only run as many concurrent requests as there are slots, which should make this 335 a rare error, assuming you are not making requests through another client. 336 """ 337 338 EXCEEDED_TIMEOUT = auto() 339 """ 340 Runtime rejection. The query has been (or surely will be) running longer than its proposed 341 ``[timeout:*]``, and has been cancelled by the server. 342 343 A higher ``[timeout:*]`` value might allow the query run to completion, but also makes it 344 more likely to be rejected by a server under heavy load, before executing it (see ``TOO_BUSY``). 345 """ 346 347 EXCEEDED_MAXSIZE = auto() 348 """ 349 Runtime rejection. The memory required to execute the query has (or surely will) exceed 350 its proposed ``[maxsize:*]``, and has been cancelled by the server. 351 352 A higher ``[maxsize:*]`` value might allow the query run to completion, but also makes it 353 more likely to be rejected by a server under heavy load, before executing it (see ``TOO_BUSY``). 354 """ 355 356 def __str__(self) -> str: 357 match self: 358 case QueryRejectCause.TOO_BUSY: 359 return "server too busy" 360 case QueryRejectCause.TOO_MANY_QUERIES: 361 return "too many queries" 362 case QueryRejectCause.EXCEEDED_TIMEOUT: 363 return "exceeded 'timeout'" 364 case QueryRejectCause.EXCEEDED_MAXSIZE: 365 return "exceeded 'maxsize'" 366 case _: 367 raise AssertionError 368 369 370@dataclass(kw_only=True) 371class QueryRejectError(QueryError): 372 """ 373 A query was rejected or cancelled by the API server. 374 375 Attributes: 376 cause: why the query was rejected or cancelled 377 timed_out_after_secs: if ``cause`` is ``EXCEEDED_TIMEOUT``, this is the amount 378 of seconds after which the query timed out 379 oom_using_mib: if ```cause`` is ``EXCEEDED_MAXSIZE``, this is the amount of RAM 380 the query used before it ran out 381 """ 382 383 cause: QueryRejectCause 384 timed_out_after_secs: int | None 385 oom_using_mib: int | None 386 387 @property 388 def should_retry(self) -> bool: 389 """Returns ``True`` if it's worth retrying when encountering this error.""" 390 return True 391 392 def __str__(self) -> str: 393 match self.cause: 394 case QueryRejectCause.TOO_BUSY | QueryRejectCause.TOO_MANY_QUERIES: 395 rejection = "rejected" 396 case QueryRejectCause.EXCEEDED_TIMEOUT: 397 rejection = f"cancelled after {self.timed_out_after_secs}s" 398 case QueryRejectCause.EXCEEDED_MAXSIZE: 399 rejection = f"cancelled using {self.oom_using_mib}" 400 case _: 401 raise AssertionError(self.cause) 402 403 return f"{rejection}: {self.cause}" 404 405 406async def _raise_for_request_error(err: aiohttp.ClientError) -> NoReturn: 407 """ 408 Raise an exception caused by the given request error. 409 410 Raises: 411 - ``CallError`` if ``obj`` is an ``aiohttp.ClientError``, 412 but not an ``aiohttp.ClientResponseError``. 413 - ``ResponseError`` otherwise. 414 """ 415 if isinstance(err, aiohttp.ClientResponseError): 416 response = err.history[-1] 417 await _raise_for_response(response, err) 418 419 raise CallError(cause=err) from err 420 421 422async def _raise_for_response( 423 response: aiohttp.ClientResponse, 424 cause: ResponseErrorCause | None, 425) -> NoReturn: 426 """Raise a ``ResponseError`` with an optional cause.""" 427 err = ResponseError( 428 response=response, 429 body=await response.text(), 430 cause=cause, 431 ) 432 if cause: 433 raise err from cause 434 raise err 435 436 437async def _result_or_raise( 438 response: aiohttp.ClientResponse, 439 query_kwargs: dict, 440 query_logger: logging.Logger, 441) -> dict: 442 """ 443 Try to extract the query result set from a response. 444 445 Raises: 446 CallError: When there is any sort of connection error. 447 RejectError: When encountering "Too Many Requests" or "Gateway Timeout"; 448 when there's a JSON remark indicating query rejection or cancellation; 449 when there's an HTML error message indicating query rejection or cancellation. 450 QueryError: When there's any other JSON remark or HTML error message. 451 ResponseError: When encountering an unexpected response. 452 """ 453 await __raise_for_plaintext_result(response) 454 455 await __raise_for_html_result(response, query_kwargs, query_logger) 456 457 return await __raise_for_json_result(response, query_kwargs, query_logger) 458 459 460async def __raise_for_json_result( 461 response: aiohttp.ClientResponse, 462 query_kwargs: dict, 463 query_logger: logging.Logger, 464) -> dict: 465 try: 466 json = await response.json() 467 if json is None: 468 await _raise_for_response(response, cause=None) 469 except aiohttp.ClientResponseError as err: 470 await _raise_for_response(response, cause=err) 471 except JSONDecodeError as err: 472 await _raise_for_response(response, cause=err) 473 474 if remark := json.get("remark"): 475 if timeout_cause := __match_reject_cause(remark, query_logger): 476 raise QueryRejectError( 477 kwargs=query_kwargs, 478 remarks=[remark], 479 cause=timeout_cause, 480 oom_using_mib=__match_oom_after(remark), 481 timed_out_after_secs=__match_timeout_after(remark), 482 ) 483 484 raise QueryResponseError( 485 response=response, 486 body=await response.text(), 487 cause=None, 488 kwargs=query_kwargs, 489 remarks=[remark], 490 ) 491 492 expected_fields = ("version", "generator", "osm3s", "elements") 493 expected_osm3s_fields = ("timestamp_osm_base", "copyright") 494 if any(f not in json for f in expected_fields) or any( 495 f not in json["osm3s"] for f in expected_osm3s_fields 496 ): 497 await _raise_for_response(response, cause=None) 498 499 return json 500 501 502async def __raise_for_html_result( 503 response: aiohttp.ClientResponse, 504 query_kwargs: dict, 505 query_logger: logging.Logger, 506) -> None: 507 """ 508 Raise a fitting exception based on error remarks in an HTML response. 509 510 Raises: 511 RejectError: if one of the error remarks indicate that the query was rejected or cancelled 512 QueryError: when encountering other error remarks 513 ResponseError: when error remarks cannot be extracted from the response 514 """ 515 if response.content_type != "text/html": 516 return 517 518 text = await response.text() 519 520 pattern = re.compile("Error</strong>: (.+?)</p>", re.DOTALL) 521 errors = [html.unescape(err.strip()) for err in pattern.findall(text)] 522 523 if not errors: # unexpected format 524 await _raise_for_response(response, cause=None) 525 526 if any(__is_ql_error(msg, query_logger) for msg in errors): 527 raise QueryLanguageError(kwargs=query_kwargs, remarks=errors) 528 529 reject_causes = [cause for err in errors if (cause := __match_reject_cause(err, query_logger))] 530 oom_using_mib = next((mib for err in errors if (mib := __match_oom_after(err))), None) 531 timed_out_after_secs = next( 532 (secs for err in errors if (secs := __match_timeout_after(err))), None 533 ) 534 535 if reject_causes: 536 raise QueryRejectError( 537 kwargs=query_kwargs, 538 remarks=errors, 539 cause=reject_causes[0], 540 oom_using_mib=oom_using_mib, 541 timed_out_after_secs=timed_out_after_secs, 542 ) 543 544 raise QueryResponseError( 545 response=response, 546 body=await response.text(), 547 cause=None, 548 kwargs=query_kwargs, 549 remarks=errors, 550 ) 551 552 553async def __raise_for_plaintext_result(response: aiohttp.ClientResponse) -> None: 554 if response.content_type != "text/plain": 555 return 556 await _raise_for_response(response, cause=None) 557 558 559def __match_reject_cause(error_msg: str, query_logger: logging.Logger) -> QueryRejectCause | None: 560 """ 561 Check if the given error message indicates that a query was rejected or cancelled. 562 563 AFAIK, neither the 'remarks' in JSON responses, nor the errors listed in HTML responses 564 are neatly listed somewhere, but it seems matching a small subset of remarks is enough 565 to identify recoverable errors. 566 567 References: 568 - Related: https://github.com/DinoTools/python-overpy/issues/62 569 - Examples in the API source: https://github.com/drolbr/Overpass-API/search?q=runtime_error 570 """ 571 if "Please check /api/status for the quota of your IP address" in error_msg: 572 cause = QueryRejectCause.TOO_MANY_QUERIES 573 574 elif "The server is probably too busy to handle your request" in error_msg: 575 cause = QueryRejectCause.TOO_BUSY 576 577 elif "Query timed out" in error_msg: 578 cause = QueryRejectCause.EXCEEDED_TIMEOUT 579 580 elif "out of memory" in error_msg: 581 cause = QueryRejectCause.EXCEEDED_MAXSIZE 582 583 else: 584 cause_cls = QueryRejectCause.__class__.__name__ 585 query_logger.debug(f"does not match any {cause_cls}: {error_msg!r}") 586 return None 587 588 query_logger.debug(f"matches {cause}: {error_msg!r}") 589 return cause 590 591 592def __match_oom_after(error_msg: str) -> int | None: 593 pattern = re.compile( 594 r"^runtime error: Query run out of memory in \".+\"" 595 r" at line \d+ using about (\d+) MB of RAM\.$" 596 ) 597 if m := pattern.match(error_msg): 598 mb = int(m.group(1)) 599 return math.ceil((mb * 1000**2) / 1024**2) 600 return None 601 602 603def __match_timeout_after(error_msg: str) -> int | None: 604 pattern = re.compile( 605 r"^runtime error: Query timed out in \".+\" at line \d+ after (\d+) seconds\.$" 606 ) 607 if m := pattern.match(error_msg): 608 return int(m.group(1)) 609 return None 610 611 612def __is_ql_error(error_msg: str, query_logger: logging.Logger) -> bool: 613 """Check if the given error message indicates that a query has bad QL code.""" 614 is_ql_error = ( 615 "encoding error:" in error_msg 616 or "parse error:" in error_msg 617 or "static error:" in error_msg 618 ) 619 620 if is_ql_error: 621 query_logger.debug(f"is a QL error: {error_msg!r}") 622 return True 623 624 query_logger.debug(f"not a QL error: {error_msg!r}") 625 return False 626 627 628def is_call_err(err: ClientError | None) -> TypeGuard[CallError]: 629 """``True`` if this is a ``CallError``.""" 630 return isinstance(err, CallError) 631 632 633def is_call_timeout(err: ClientError | None) -> TypeGuard[CallTimeoutError]: 634 """``True`` if this is a ``CallTimeoutError``.""" 635 return isinstance(err, CallTimeoutError) 636 637 638def is_giveup_err(err: ClientError | None) -> TypeGuard[GiveupError]: 639 """``True`` if this is a ``GiveupError``.""" 640 return isinstance(err, GiveupError) 641 642 643def is_server_error(err: ClientError | None) -> TypeGuard[ResponseError]: 644 """``True`` if this is a ``ResponseError`` presumably cause by a server-side error.""" 645 return isinstance(err, ResponseError) and err.is_server_error 646 647 648def is_rejection(err: ClientError | None) -> TypeGuard[QueryRejectError]: 649 """``True`` if this is a ``QueryRejectError``.""" 650 return isinstance(err, QueryRejectError) 651 652 653def is_gateway_rejection(err: ClientError | None) -> TypeGuard[QueryRejectError]: 654 """``True`` if this is a ``QueryRejectError`` with gateway rejection.""" 655 return isinstance(err, QueryRejectError) and err.cause in { 656 QueryRejectCause.TOO_MANY_QUERIES, 657 QueryRejectCause.TOO_BUSY, 658 } 659 660 661def is_too_many_queries(err: ClientError | None) -> TypeGuard[QueryRejectError]: 662 """``True`` if this is a ``QueryRejectError`` with cause ``TOO_MANY_QUERIES``.""" 663 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.TOO_MANY_QUERIES 664 665 666def is_too_busy(err: ClientError | None) -> TypeGuard[QueryRejectError]: 667 """``True`` if this is a ``QueryRejectError`` with cause ``TOO_BUSY``.""" 668 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.TOO_BUSY 669 670 671def is_runtime_rejection(err: ClientError | None) -> TypeGuard[QueryRejectError]: 672 """``True`` if this is a ``QueryRejectError`` with runtime rejection.""" 673 return isinstance(err, QueryRejectError) and err.cause in { 674 QueryRejectCause.EXCEEDED_MAXSIZE, 675 QueryRejectCause.EXCEEDED_TIMEOUT, 676 } 677 678 679def is_exceeding_maxsize(err: ClientError | None) -> TypeGuard[QueryRejectError]: 680 """``True`` if this is a ``GiveupError`` with cause ``EXCEEDED_MAXSIZE``.""" 681 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.EXCEEDED_MAXSIZE 682 683 684def is_exceeding_timeout(err: ClientError | None) -> TypeGuard[QueryRejectError]: 685 """``True`` if this is a ``GiveupError`` with cause ``EXCEEDED_TIMEOUT``.""" 686 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.EXCEEDED_TIMEOUT
62@dataclass(kw_only=True) 63class AlreadyRunningError(ValueError): 64 """ 65 Raised when trying to run a query while it's already being run. 66 67 Attributes: 68 kwargs: the query's ``kwargs`` 69 """ 70 71 kwargs: dict 72 73 def __str__(self) -> str: 74 return f"query{self.kwargs!r} is already being run"
Raised when trying to run a query while it's already being run.
Attributes:
- kwargs: the query's
kwargs
77class ClientError(Exception): 78 """Base exception for failed Overpass API requests and queries.""" 79 80 @property 81 def should_retry(self) -> bool: 82 """Returns ``True`` if it's worth retrying when encountering this error.""" 83 return False
Base exception for failed Overpass API requests and queries.
110@dataclass(kw_only=True) 111class CallError(ClientError): 112 """ 113 Failed to make an API request. 114 115 This error is raised when the client failed to get any response, 116 f.e. due to connection issues. 117 118 Attributes: 119 cause: the exception that caused this error 120 """ 121 122 cause: aiohttp.ClientError | asyncio.TimeoutError 123 124 @property 125 def should_retry(self) -> bool: 126 """Returns ``True`` if it's worth retrying when encountering this error.""" 127 return True 128 129 def __str__(self) -> str: 130 return str(self.cause)
Failed to make an API request.
This error is raised when the client failed to get any response, f.e. due to connection issues.
Attributes:
- cause: the exception that caused this error
133@dataclass(kw_only=True) 134class CallTimeoutError(CallError): 135 """ 136 An API request timed out. 137 138 Attributes: 139 cause: the exception that caused this error 140 after_secs: the configured timeout for the request 141 """ 142 143 cause: asyncio.TimeoutError 144 after_secs: float 145 146 @property 147 def should_retry(self) -> bool: 148 """Returns ``True`` if it's worth retrying when encountering this error.""" 149 return True 150 151 def __str__(self) -> str: 152 return str(self.cause)
An API request timed out.
Attributes:
- cause: the exception that caused this error
- after_secs: the configured timeout for the request
159@dataclass(kw_only=True) 160class ResponseError(ClientError): 161 """ 162 Unexpected API response. 163 164 On the one hand, this can be an error that happened on the Overpass API instance, 165 which is usually signalled by a status code ``>= 500``. In rare cases, 166 the ``cause`` being a ``JSONDecodeError`` also signals this, since it can happen 167 that the API returns a cutoff JSON response. In both of these cases, 168 ``is_server_error()`` will return ``True``. 169 170 On the other hand, this error may indicate that a request failed, but we can't 171 specifically say why. This could be a bug on our end, since the client 172 is meant to process almost any response of an Overpass API instance. 173 Here, ``is_server_error()`` will return ``False``, and the default query runner 174 will log the response body. 175 176 Attributes: 177 response: the unexpected response 178 body: the response body 179 cause: an optional exception that may have caused this error 180 """ 181 182 response: aiohttp.ClientResponse 183 body: str 184 cause: ResponseErrorCause | None 185 186 @property 187 def should_retry(self) -> bool: 188 """Returns ``True`` if it's worth retrying when encountering this error.""" 189 return True 190 191 @property 192 def is_server_error(self) -> bool: 193 """Returns ``True`` if this presumably a server-side error.""" 194 # see class doc for details 195 return self.response.status >= 500 or isinstance(self.cause, JSONDecodeError) 196 197 def __str__(self) -> str: 198 if self.cause is None: 199 return f"unexpected response ({self.response.status})" 200 return f"unexpected response ({self.response.status}): {self.cause}"
Unexpected API response.
On the one hand, this can be an error that happened on the Overpass API instance,
which is usually signalled by a status code >= 500
. In rare cases,
the cause
being a JSONDecodeError
also signals this, since it can happen
that the API returns a cutoff JSON response. In both of these cases,
is_server_error()
will return True
.
On the other hand, this error may indicate that a request failed, but we can't
specifically say why. This could be a bug on our end, since the client
is meant to process almost any response of an Overpass API instance.
Here, is_server_error()
will return False
, and the default query runner
will log the response body.
Attributes:
- response: the unexpected response
- body: the response body
- cause: an optional exception that may have caused this error
186 @property 187 def should_retry(self) -> bool: 188 """Returns ``True`` if it's worth retrying when encountering this error.""" 189 return True
Returns True
if it's worth retrying when encountering this error.
191 @property 192 def is_server_error(self) -> bool: 193 """Returns ``True`` if this presumably a server-side error.""" 194 # see class doc for details 195 return self.response.status >= 500 or isinstance(self.cause, JSONDecodeError)
Returns True
if this presumably a server-side error.
Causes for a ResponseError
.
232@dataclass(kw_only=True) 233class GiveupError(ClientError): 234 """ 235 The client spent or would spend too long running a query, and gave up. 236 237 This error is raised when the run timeout duration set by a query runner 238 is or would be exceeded. 239 240 Attributes: 241 kwargs: the query's ``kwargs`` 242 after_secs: the total time spent on the query 243 cause: why the query was given up on 244 """ 245 246 kwargs: dict 247 after_secs: float 248 cause: GiveupCause 249 250 @property 251 def should_retry(self) -> bool: 252 """Returns ``True`` if it's worth retrying when encountering this error.""" 253 return False 254 255 def __str__(self) -> str: 256 return f"gave up on query{self.kwargs!r} after {self.after_secs:.01f} seconds"
The client spent or would spend too long running a query, and gave up.
This error is raised when the run timeout duration set by a query runner is or would be exceeded.
Attributes:
- kwargs: the query's
kwargs
- after_secs: the total time spent on the query
- cause: why the query was given up on
203class GiveupCause(Enum): 204 """Details on why a query was given up on.""" 205 206 RUN_TIMEOUT_BEFORE_STATUS_CALL = auto() 207 """Query's ``run_timeout_secs`` elapsed before a status call before the actual query call.""" 208 209 RUN_TIMEOUT_BY_COOLDOWN = auto() 210 """Query received a timeout that would exceed its remaining ``run_timeout_secs``.""" 211 212 RUN_TIMEOUT_BEFORE_QUERY_CALL = auto() 213 """Query's ``run_timeout_secs`` elapsed before the query call.""" 214 215 EXPECTING_QUERY_TIMEOUT = auto() 216 """ 217 The query's limited [timeout:*] setting is likely too low. 218 219 A query's [timeout:*] setting might be lowered by the client to the remainder of 220 ``run_timeout_secs``. If that happens, and that duration is also lower than a duration 221 after which it was cancelled at the server previously, then this is the cause to give 222 up on the query. 223 224 See Also: 225 ``QueryRejectError.timed_out_after_secs`` 226 """ 227 228 RUN_TIMEOUT_DURING_QUERY_CALL = auto() 229 """Query's ``run_timeout_secs`` elapsed while making the query call, before any response."""
Details on why a query was given up on.
Query's run_timeout_secs
elapsed before a status call before the actual query call.
Query received a timeout that would exceed its remaining run_timeout_secs
.
Query's run_timeout_secs
elapsed before the query call.
The query's limited [timeout:*] setting is likely too low.
A query's [timeout:*] setting might be lowered by the client to the remainder of
run_timeout_secs
. If that happens, and that duration is also lower than a duration
after which it was cancelled at the server previously, then this is the cause to give
up on the query.
See Also:
Query's run_timeout_secs
elapsed while making the query call, before any response.
259@dataclass(kw_only=True) 260class QueryError(ClientError): 261 """ 262 Base exception for queries that failed at the Overpass API server. 263 264 Attributes: 265 kwargs: the query's ``kwargs`` 266 remarks: the error remarks provided by the API 267 """ 268 269 kwargs: dict 270 remarks: list[str] 271 272 @property 273 def should_retry(self) -> bool: 274 """Returns ``True`` if it's worth retrying when encountering this error.""" 275 return False 276 277 def __str__(self) -> str: 278 first = f"'{self.remarks[0]}'" 279 rest = f" (+{len(self.remarks) - 1} more)" if len(self.remarks) > 1 else "" 280 return f"query{self.kwargs!r} failed: {first}{rest}"
Base exception for queries that failed at the Overpass API server.
Attributes:
- kwargs: the query's
kwargs
- remarks: the error remarks provided by the API
306@dataclass(kw_only=True) 307class QueryLanguageError(QueryError): 308 """ 309 Indicates the query's QL code is not valid. 310 311 Retrying is pointless when encountering this error. 312 """ 313 314 @property 315 def should_retry(self) -> bool: 316 """Returns ``True`` if it's worth retrying when encountering this error.""" 317 return False
Indicates the query's QL code is not valid.
Retrying is pointless when encountering this error.
314 @property 315 def should_retry(self) -> bool: 316 """Returns ``True`` if it's worth retrying when encountering this error.""" 317 return False
Returns True
if it's worth retrying when encountering this error.
Inherited Members
371@dataclass(kw_only=True) 372class QueryRejectError(QueryError): 373 """ 374 A query was rejected or cancelled by the API server. 375 376 Attributes: 377 cause: why the query was rejected or cancelled 378 timed_out_after_secs: if ``cause`` is ``EXCEEDED_TIMEOUT``, this is the amount 379 of seconds after which the query timed out 380 oom_using_mib: if ```cause`` is ``EXCEEDED_MAXSIZE``, this is the amount of RAM 381 the query used before it ran out 382 """ 383 384 cause: QueryRejectCause 385 timed_out_after_secs: int | None 386 oom_using_mib: int | None 387 388 @property 389 def should_retry(self) -> bool: 390 """Returns ``True`` if it's worth retrying when encountering this error.""" 391 return True 392 393 def __str__(self) -> str: 394 match self.cause: 395 case QueryRejectCause.TOO_BUSY | QueryRejectCause.TOO_MANY_QUERIES: 396 rejection = "rejected" 397 case QueryRejectCause.EXCEEDED_TIMEOUT: 398 rejection = f"cancelled after {self.timed_out_after_secs}s" 399 case QueryRejectCause.EXCEEDED_MAXSIZE: 400 rejection = f"cancelled using {self.oom_using_mib}" 401 case _: 402 raise AssertionError(self.cause) 403 404 return f"{rejection}: {self.cause}"
A query was rejected or cancelled by the API server.
Attributes:
388 @property 389 def should_retry(self) -> bool: 390 """Returns ``True`` if it's worth retrying when encountering this error.""" 391 return True
Returns True
if it's worth retrying when encountering this error.
Inherited Members
320class QueryRejectCause(Enum): 321 """Details why a query was rejected or cancelled by an API server.""" 322 323 TOO_BUSY = auto() 324 """ 325 Gateway rejection. The server has already so much load that the request cannot be executed. 326 327 Smaller ``[timeout:*]`` and/or ``[maxsize:*]`` values might make the request acceptable. 328 """ 329 330 TOO_MANY_QUERIES = auto() 331 """ 332 Gateway rejection. There are no open slots for queries from your IP address. 333 334 Running queries take up a slot, and the number of slots is limited. A client 335 will only run as many concurrent requests as there are slots, which should make this 336 a rare error, assuming you are not making requests through another client. 337 """ 338 339 EXCEEDED_TIMEOUT = auto() 340 """ 341 Runtime rejection. The query has been (or surely will be) running longer than its proposed 342 ``[timeout:*]``, and has been cancelled by the server. 343 344 A higher ``[timeout:*]`` value might allow the query run to completion, but also makes it 345 more likely to be rejected by a server under heavy load, before executing it (see ``TOO_BUSY``). 346 """ 347 348 EXCEEDED_MAXSIZE = auto() 349 """ 350 Runtime rejection. The memory required to execute the query has (or surely will) exceed 351 its proposed ``[maxsize:*]``, and has been cancelled by the server. 352 353 A higher ``[maxsize:*]`` value might allow the query run to completion, but also makes it 354 more likely to be rejected by a server under heavy load, before executing it (see ``TOO_BUSY``). 355 """ 356 357 def __str__(self) -> str: 358 match self: 359 case QueryRejectCause.TOO_BUSY: 360 return "server too busy" 361 case QueryRejectCause.TOO_MANY_QUERIES: 362 return "too many queries" 363 case QueryRejectCause.EXCEEDED_TIMEOUT: 364 return "exceeded 'timeout'" 365 case QueryRejectCause.EXCEEDED_MAXSIZE: 366 return "exceeded 'maxsize'" 367 case _: 368 raise AssertionError
Details why a query was rejected or cancelled by an API server.
Gateway rejection. The server has already so much load that the request cannot be executed.
Smaller [timeout:*]
and/or [maxsize:*]
values might make the request acceptable.
Gateway rejection. There are no open slots for queries from your IP address.
Running queries take up a slot, and the number of slots is limited. A client will only run as many concurrent requests as there are slots, which should make this a rare error, assuming you are not making requests through another client.
Runtime rejection. The query has been (or surely will be) running longer than its proposed
[timeout:*]
, and has been cancelled by the server.
A higher [timeout:*]
value might allow the query run to completion, but also makes it
more likely to be rejected by a server under heavy load, before executing it (see TOO_BUSY
).
Runtime rejection. The memory required to execute the query has (or surely will) exceed
its proposed [maxsize:*]
, and has been cancelled by the server.
A higher [maxsize:*]
value might allow the query run to completion, but also makes it
more likely to be rejected by a server under heavy load, before executing it (see TOO_BUSY
).
283@dataclass(kw_only=True) 284class QueryResponseError(ResponseError, QueryError): 285 """ 286 Unexpected query response. 287 288 This error is raised when a query fails (thus extends ``QueryError``), 289 but we can't specifically say why (thus also extends ``ResponseError``). 290 """ 291 292 @property 293 def should_retry(self) -> bool: 294 """Returns ``True`` if it's worth retrying when encountering this error.""" 295 return ResponseError.should_retry.fget(self) # type: ignore[attr-defined] 296 297 def __str__(self) -> str: 298 if self.remarks: 299 first = f"'{self.remarks[0]}'" 300 rest = f" (+{len(self.remarks) - 1} more)" if len(self.remarks) > 1 else "" 301 return f"query{self.kwargs!r} failed: {first}{rest}" 302 303 return f"query{self.kwargs!r} failed with status {self.response.status}"
Unexpected query response.
This error is raised when a query fails (thus extends QueryError
),
but we can't specifically say why (thus also extends ResponseError
).
292 @property 293 def should_retry(self) -> bool: 294 """Returns ``True`` if it's worth retrying when encountering this error.""" 295 return ResponseError.should_retry.fget(self) # type: ignore[attr-defined]
Returns True
if it's worth retrying when encountering this error.
Inherited Members
86@dataclass(kw_only=True) 87class RunnerError(ClientError): 88 """ 89 The query runner raised an exception. 90 91 This is an unexpected error, in contrast to the runner intentionally 92 raising ``query.error`` to stop retrying. The cause of this error 93 is therefore anything but a ``ClientError``. 94 95 Attributes: 96 cause: the exception that caused this error 97 """ 98 99 cause: BaseException 100 101 @property 102 def should_retry(self) -> bool: 103 """Returns ``True`` if it's worth retrying when encountering this error.""" 104 return False 105 106 def __str__(self) -> str: 107 return str(self.cause)
The query runner raised an exception.
This is an unexpected error, in contrast to the runner intentionally
raising query.error
to stop retrying. The cause of this error
is therefore anything but a ClientError
.
Attributes:
- cause: the exception that caused this error
629def is_call_err(err: ClientError | None) -> TypeGuard[CallError]: 630 """``True`` if this is a ``CallError``.""" 631 return isinstance(err, CallError)
True
if this is a CallError
.
634def is_call_timeout(err: ClientError | None) -> TypeGuard[CallTimeoutError]: 635 """``True`` if this is a ``CallTimeoutError``.""" 636 return isinstance(err, CallTimeoutError)
True
if this is a CallTimeoutError
.
680def is_exceeding_maxsize(err: ClientError | None) -> TypeGuard[QueryRejectError]: 681 """``True`` if this is a ``GiveupError`` with cause ``EXCEEDED_MAXSIZE``.""" 682 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.EXCEEDED_MAXSIZE
True
if this is a GiveupError
with cause EXCEEDED_MAXSIZE
.
685def is_exceeding_timeout(err: ClientError | None) -> TypeGuard[QueryRejectError]: 686 """``True`` if this is a ``GiveupError`` with cause ``EXCEEDED_TIMEOUT``.""" 687 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.EXCEEDED_TIMEOUT
True
if this is a GiveupError
with cause EXCEEDED_TIMEOUT
.
654def is_gateway_rejection(err: ClientError | None) -> TypeGuard[QueryRejectError]: 655 """``True`` if this is a ``QueryRejectError`` with gateway rejection.""" 656 return isinstance(err, QueryRejectError) and err.cause in { 657 QueryRejectCause.TOO_MANY_QUERIES, 658 QueryRejectCause.TOO_BUSY, 659 }
True
if this is a QueryRejectError
with gateway rejection.
639def is_giveup_err(err: ClientError | None) -> TypeGuard[GiveupError]: 640 """``True`` if this is a ``GiveupError``.""" 641 return isinstance(err, GiveupError)
True
if this is a GiveupError
.
649def is_rejection(err: ClientError | None) -> TypeGuard[QueryRejectError]: 650 """``True`` if this is a ``QueryRejectError``.""" 651 return isinstance(err, QueryRejectError)
True
if this is a QueryRejectError
.
672def is_runtime_rejection(err: ClientError | None) -> TypeGuard[QueryRejectError]: 673 """``True`` if this is a ``QueryRejectError`` with runtime rejection.""" 674 return isinstance(err, QueryRejectError) and err.cause in { 675 QueryRejectCause.EXCEEDED_MAXSIZE, 676 QueryRejectCause.EXCEEDED_TIMEOUT, 677 }
True
if this is a QueryRejectError
with runtime rejection.
644def is_server_error(err: ClientError | None) -> TypeGuard[ResponseError]: 645 """``True`` if this is a ``ResponseError`` presumably cause by a server-side error.""" 646 return isinstance(err, ResponseError) and err.is_server_error
True
if this is a ResponseError
presumably cause by a server-side error.
667def is_too_busy(err: ClientError | None) -> TypeGuard[QueryRejectError]: 668 """``True`` if this is a ``QueryRejectError`` with cause ``TOO_BUSY``.""" 669 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.TOO_BUSY
True
if this is a QueryRejectError
with cause TOO_BUSY
.
662def is_too_many_queries(err: ClientError | None) -> TypeGuard[QueryRejectError]: 663 """``True`` if this is a ``QueryRejectError`` with cause ``TOO_MANY_QUERIES``.""" 664 return isinstance(err, QueryRejectError) and err.cause is QueryRejectCause.TOO_MANY_QUERIES
True
if this is a QueryRejectError
with cause TOO_MANY_QUERIES
.