|
| 1 | +"""Exception taxonomy for ``dataretrieval``. |
| 2 | +
|
| 3 | +A failed request from any service module (``nwis``, ``wqp``, ``waterdata``, |
| 4 | +``nldi``, ...) raises a subclass of :class:`DataRetrievalError`, so a caller can |
| 5 | +handle any request failure with a single ``except dataretrieval.DataRetrievalError``. |
| 6 | +
|
| 7 | +This module deliberately has no third-party dependencies, so any module can |
| 8 | +import it without pulling in pandas/httpx. |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | + |
| 13 | +__all__ = [ |
| 14 | + "DataRetrievalError", |
| 15 | + "BadRequestError", |
| 16 | + "NotFoundError", |
| 17 | + "RequestTooLargeError", |
| 18 | + "ServiceUnavailableError", |
| 19 | + "NoSitesError", |
| 20 | + "RateLimited", |
| 21 | + "ServiceUnavailable", |
| 22 | + "RequestTooLarge", |
| 23 | +] |
| 24 | + |
| 25 | + |
| 26 | +class DataRetrievalError(Exception): |
| 27 | + """Base class for errors raised when a request to a USGS or EPA web |
| 28 | + service fails. |
| 29 | +
|
| 30 | + Every service module (``nwis``, ``wqp``, ``waterdata``, ``nldi``, ...) |
| 31 | + raises a subclass of this when a request fails, so a caller can handle any |
| 32 | + request failure uniformly:: |
| 33 | +
|
| 34 | + try: |
| 35 | + df, md = dataretrieval.wqp.get_results(...) |
| 36 | + except dataretrieval.DataRetrievalError: |
| 37 | + ... |
| 38 | +
|
| 39 | + Subclasses also inherit from the built-in exception this package has |
| 40 | + historically raised for the same condition (e.g. :class:`BadRequestError` |
| 41 | + is also a :class:`ValueError`, :class:`RateLimited` is also a |
| 42 | + :class:`RuntimeError`), so existing ``except ValueError`` / ``except |
| 43 | + RuntimeError`` handlers keep working unchanged. |
| 44 | + """ |
| 45 | + |
| 46 | + |
| 47 | +# Legacy ``query()`` path: HTTP status families mapped to ValueError-compatible |
| 48 | +# types (the type that path has always raised). |
| 49 | +class BadRequestError(DataRetrievalError, ValueError): |
| 50 | + """The service rejected the request parameters (HTTP 400).""" |
| 51 | + |
| 52 | + |
| 53 | +class NotFoundError(DataRetrievalError, ValueError): |
| 54 | + """The requested resource was not found; often an empty query (HTTP 404).""" |
| 55 | + |
| 56 | + |
| 57 | +class RequestTooLargeError(DataRetrievalError, ValueError): |
| 58 | + """The request URL was too long for the service (HTTP 414, or rejected |
| 59 | + client-side before it was sent).""" |
| 60 | + |
| 61 | + |
| 62 | +class ServiceUnavailableError(DataRetrievalError, ValueError): |
| 63 | + """The service is down or returned a server error (HTTP 5xx).""" |
| 64 | + |
| 65 | + |
| 66 | +class NoSitesError(DataRetrievalError): |
| 67 | + """The selection criteria matched no sites/data.""" |
| 68 | + |
| 69 | + def __init__(self, url): |
| 70 | + self.url = url |
| 71 | + |
| 72 | + def __str__(self): |
| 73 | + return ( |
| 74 | + "No sites/data found using the selection criteria specified in " |
| 75 | + f"url: {self.url}" |
| 76 | + ) |
| 77 | + |
| 78 | + |
| 79 | +# Water Data API transport errors: retryable HTTP status families, surfaced as |
| 80 | +# RuntimeError-compatible types the chunker detects via ``isinstance`` and wraps |
| 81 | +# as resumable interruptions. |
| 82 | +class _RetryableTransportError(DataRetrievalError, RuntimeError): |
| 83 | + """ |
| 84 | + Base for typed HTTP transport failures the chunker recognizes as |
| 85 | + transient. |
| 86 | +
|
| 87 | + Raised by :func:`dataretrieval.waterdata.utils._raise_for_non_200` |
| 88 | + and walked by :func:`dataretrieval.waterdata.chunking._classify_chunk_error`. |
| 89 | + One subclass per recoverable HTTP status family (429 → :class:`RateLimited`, |
| 90 | + 5xx → :class:`ServiceUnavailable`); ``ChunkedCall`` wraps them as resumable |
| 91 | + :class:`~dataretrieval.waterdata.chunking.ChunkInterrupted` subclasses. |
| 92 | +
|
| 93 | + Parameters |
| 94 | + ---------- |
| 95 | + message : str |
| 96 | + Human-readable error message. |
| 97 | + retry_after : float, optional |
| 98 | + Seconds to wait before retrying, parsed from the |
| 99 | + ``Retry-After`` response header. |
| 100 | +
|
| 101 | + Attributes |
| 102 | + ---------- |
| 103 | + retry_after : float or None |
| 104 | + Seconds to wait before retrying, parsed from the |
| 105 | + ``Retry-After`` response header. ``None`` when the header was |
| 106 | + absent or unparseable. |
| 107 | + """ |
| 108 | + |
| 109 | + def __init__(self, message: str, *, retry_after: float | None = None) -> None: |
| 110 | + super().__init__(message) |
| 111 | + self.retry_after = retry_after |
| 112 | + |
| 113 | + |
| 114 | +class RateLimited(_RetryableTransportError): |
| 115 | + """ |
| 116 | + A USGS Water Data API request was rejected with HTTP 429. |
| 117 | +
|
| 118 | + Exposed as a typed exception so callers (notably the multi-value |
| 119 | + chunker) can detect rate-limit failures via ``isinstance`` instead |
| 120 | + of string-matching error messages. |
| 121 | + """ |
| 122 | + |
| 123 | + |
| 124 | +class ServiceUnavailable(_RetryableTransportError): |
| 125 | + """ |
| 126 | + A USGS Water Data API request was rejected with HTTP 5xx. |
| 127 | +
|
| 128 | + Surfaced as a typed exception (parallel to :class:`RateLimited`) |
| 129 | + so ``ChunkedCall`` can treat transient server failures as |
| 130 | + resumable interruptions rather than fatal programmer errors. |
| 131 | + """ |
| 132 | + |
| 133 | + |
| 134 | +class RequestTooLarge(DataRetrievalError, ValueError): |
| 135 | + """ |
| 136 | + No chunking plan fits the URL byte limit. |
| 137 | +
|
| 138 | + Raised when even the smallest reducible plan (every list axis at |
| 139 | + singleton chunks and the filter at one clause per sub-request) |
| 140 | + still exceeds the server's byte limit. Shrink the input lists, |
| 141 | + simplify the filter, or split the call manually. |
| 142 | + """ |
0 commit comments