diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a34b7d..2436a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,22 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.4] + +### Added +- `TooManyRequestsError` is raised by all API calls when the TIND API returns an HTTP 429 response. + + ## [0.2.3] ### Added - parameter to client.write_search_results_to_file() to specify an output directory for the file, with fallback to default_storage_dir if not provided - also uses PATH + ## [0.2.2] ### Changed - build package and publish to pypi, using setuptools_scm for version management + ## [0.2.1] ### Changed - reconciling version agreement between the releases, this file, and the pyproject.toml + ## [0.2.0] ### Added diff --git a/tests/test_api.py b/tests/test_api.py index 8382f7f..0a1da0c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,7 +6,7 @@ import requests_mock as req_mock # noqa: F401 — activates the requests_mock fixture from tind_client.api import tind_download, tind_get -from tind_client.errors import AuthorizationError +from tind_client.errors import AuthorizationError, TooManyRequestsError BASE_URL = "https://tind.example.edu" API_KEY = "test-api-key" @@ -39,6 +39,13 @@ def test_tind_get_raises_on_401(requests_mock: req_mock.Mocker) -> None: tind_get("record/1/", api_key=API_KEY, api_url=BASE_URL) +def test_tind_get_raises_on_429(requests_mock: req_mock.Mocker) -> None: + """Ensure tind_get raises TooManyRequestsError on HTTP 429.""" + requests_mock.get(f"{BASE_URL}/record/1/", status_code=429) + with pytest.raises(TooManyRequestsError): + tind_get("record/1/", api_key=API_KEY, api_url=BASE_URL) + + def test_tind_get_raises_on_500(requests_mock: req_mock.Mocker) -> None: """tind_get propagates an HTTPError on HTTP 5xx.""" requests_mock.get(f"{BASE_URL}/record/1/", status_code=500) @@ -90,6 +97,14 @@ def test_tind_download_raises_on_401(requests_mock: req_mock.Mocker) -> None: tind_download(url, "/tmp", api_key=API_KEY) +def test_tind_download_raises_on_429(requests_mock: req_mock.Mocker) -> None: + """Ensure tind_download raises TooManyRequestsError on HTTP 429.""" + url = f"{BASE_URL}/files/12345/download" + requests_mock.get(url, status_code=429) + with pytest.raises(TooManyRequestsError): + tind_download(url, "/tmp", api_key=API_KEY) + + def test_tind_download_non_200_returns_empty(requests_mock: req_mock.Mocker) -> None: """tind_download returns (status, '') for non-200, non-error responses.""" url = f"{BASE_URL}/files/12345/download" diff --git a/tind_client/api.py b/tind_client/api.py index 0828be1..2d7105f 100644 --- a/tind_client/api.py +++ b/tind_client/api.py @@ -7,7 +7,7 @@ from typing import Tuple import requests -from .errors import AuthorizationError, TINDError +from .errors import AuthorizationError, TINDError, TooManyRequestsError TIMEOUT: int = 30 @@ -39,6 +39,8 @@ def tind_get( :param dict|None params: Extra query parameters to send. For example, ``{'of': 'xm'}``. :raises AuthorizationError: If an invalid TIND API key is provided. + :raises TooManyRequestsError: If the TIND server is overloaded with requests. + :raises TINDError: If an internal server error occurs during request processing. :returns: A tuple of the HTTP status code and response text (if any). :rtype: Tuple[int, str] """ @@ -54,6 +56,8 @@ def tind_get( ) if resp.status_code == 401: raise AuthorizationError("Invalid TIND API key provided") + if resp.status_code == 429: + raise TooManyRequestsError("Enhance your calm") if resp.status_code >= 500: raise TINDError.from_json(resp.status_code, resp.text) return resp.status_code, resp.text @@ -66,6 +70,8 @@ def tind_download(url: str, output_dir: str, api_key: str) -> Tuple[int, str]: :param str output_dir: The path to the directory in which to save the file. :param str api_key: The TIND API token. :raises AuthorizationError: If an invalid TIND API key is provided. + :raises TooManyRequestsError: If the TIND server is overloaded with requests. + :raises TINDError: If an internal server error occurs during request processing. :returns: A tuple of the HTTP status code and the path to the downloaded file (if successful). :rtype: Tuple[int, str] """ @@ -73,6 +79,8 @@ def tind_download(url: str, output_dir: str, api_key: str) -> Tuple[int, str]: status = resp.status_code if status == 401: raise AuthorizationError("Invalid TIND API key provided") + if status == 429: + raise TooManyRequestsError("Enhance your calm") if status >= 500: raise TINDError.from_json(status, resp.text) if status != 200: diff --git a/tind_client/errors.py b/tind_client/errors.py index 6a3998a..ac2bab0 100644 --- a/tind_client/errors.py +++ b/tind_client/errors.py @@ -28,5 +28,9 @@ class AuthorizationError(TINDError): """Raised when authorization with the TIND API fails.""" +class TooManyRequestsError(TINDError): + """Raised when the TIND API has had too many requests in a short period.""" + + class RecordNotFoundError(TINDError): """Raised when a requested record or file is not found in TIND."""