From 46999b8b009b44457239df3f2441a3e5def1f854 Mon Sep 17 00:00:00 2001 From: Oliver Fuerst Date: Thu, 14 May 2026 15:26:58 +0100 Subject: [PATCH] feat: Make raise_for_status configurable Defaults to the original behaviour of raising for status when making a request to the EPO OPS API --- README.md | 8 ++-- epo_ops/api.py | 24 +++++++----- tests/test_api.py | 8 ++++ tests/test_raise_for_status.py | 67 ++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 tests/test_raise_for_status.py diff --git a/README.md b/README.md index b54553f..abad666 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,11 @@ you'll interact with mostly. When you issue a request, the response is a [requests.Response][] object. If `response.status_code != 200` then a `requests.HTTPError` exception will be -raised — it's your responsibility to handle those exceptions if you want to. The -one case that's handled is when the access token has expired: in this case, the -client will automatically handle the HTTP 400 status and renew the token. +raised — it's your responsibility to handle those exceptions if you want to. +This default can be disabled by passing `raise_for_status=False` when +constructing the client. The one case that's handled is when the access token +has expired: in this case, the client will automatically handle the HTTP 400 +status and renew the token. Note that the Client does not attempt to interpret the data supplied by OPS, so it's your responsibility to parse the XML or JSON payload for your own purpose. diff --git a/epo_ops/api.py b/epo_ops/api.py index 2ec90e4..d96a741 100644 --- a/epo_ops/api.py +++ b/epo_ops/api.py @@ -10,18 +10,13 @@ from . import exceptions from .middlewares import Throttler -from .models import ( - AccessToken, - Docdb, - Epodoc, - Original, - Request, -) +from .models import AccessToken, Docdb, Epodoc, Original, Request log = logging.getLogger(__name__) DEFAULT_NETWORK_TIMEOUT = 10.0 + class Client(object): __auth_url__ = "https://ops.epo.org/3.2/auth/accesstoken" __service_url_prefix__ = "https://ops.epo.org/3.2/rest-services" @@ -35,7 +30,15 @@ class Client(object): __register_path__ = "register" __register_search_path__ = "register/search" - def __init__(self, key, secret, accept_type="xml", middlewares=None, timeout=DEFAULT_NETWORK_TIMEOUT): + def __init__( + self, + key, + secret, + accept_type="xml", + middlewares=None, + timeout=DEFAULT_NETWORK_TIMEOUT, + raise_for_status=True, + ): self.accept_type = "application/{0}".format(accept_type) self.middlewares = middlewares if middlewares is None: @@ -44,6 +47,7 @@ def __init__(self, key, secret, accept_type="xml", middlewares=None, timeout=DEF self.key = key self.secret = secret self.timeout = timeout + self.raise_for_status = raise_for_status self._access_token = None def family( @@ -154,6 +158,7 @@ def legal( input=input, ) ) + def number( self, reference_type: str, @@ -395,7 +400,8 @@ def _make_request( ) response = self._check_for_expired_token(response) response = self._check_for_exceeded_quota(response) - response.raise_for_status() + if self.raise_for_status: + response.raise_for_status() return response # info: { diff --git a/tests/test_api.py b/tests/test_api.py index 1067723..0579e51 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,6 +28,12 @@ def test_instantiate_simple_client(): client = Client("key", "secret") assert len(client.middlewares) == 1 assert client.middlewares[0].history.db_path == sqlite.DEFAULT_DB_PATH + assert client.raise_for_status is True + + +def test_instantiate_client_without_raise_for_status(): + client = Client("key", "secret", raise_for_status=False) + assert client.raise_for_status is False def test_family(all_clients): @@ -45,9 +51,11 @@ def test_family_legal(all_clients): def test_image(all_clients): assert_image_success(all_clients) + def test_legal(all_clients): assert_legal_success(all_clients) + def test_published_data(all_clients): assert_published_data_success(all_clients) diff --git a/tests/test_raise_for_status.py b/tests/test_raise_for_status.py new file mode 100644 index 0000000..ab4544d --- /dev/null +++ b/tests/test_raise_for_status.py @@ -0,0 +1,67 @@ +import pytest +import responses +from pytest import raises +from requests.exceptions import HTTPError + +from epo_ops.api import Client +from epo_ops.models import Docdb + + +@pytest.fixture +def ops_backend_413(): + """ + Emulate an OPS backend returning 413 on the fulltext endpoint for an + ambiguous input. The real upstream returns 413 for e.g. EP.0536425/fulltext. + """ + token = responses.Response( + responses.POST, + url="https://ops.epo.org/3.2/auth/accesstoken", + status=200, + json={"access_token": "foo", "expires_in": 42}, + ) + fulltext_413 = responses.Response( + responses.POST, + url="https://ops.epo.org/3.2/rest-services/published-data/publication/docdb/fulltext", + status=413, + headers={"Content-Type": "application/xml"}, + body=( + '\n' + ' \n' + " CLIENT.AmbiguousRequest\n" + " The request was ambiguous\n" + "
\n" + " Ambiguous input: publication/docdb/EP.0536425\n" + " publication/docdb/EP.0536425.A1\n" + " publication/docdb/EP.0536425.A4\n" + " publication/docdb/EP.0536425.B1\n" + " publication/docdb/EP.0536425.B2\n" + "
\n" + "
" + ), + ) + for response in [token, fulltext_413]: + responses.add(response) + + +def _issue_fulltext_request(client): + return client.published_data( + "publication", + Docdb("0536425", "EP", "B1"), + endpoint="fulltext", + ) + + +@responses.activate +def test_413_raises_by_default(ops_backend_413): + client = Client("key", "secret", middlewares=[]) + with raises(HTTPError): + _issue_fulltext_request(client) + + +@responses.activate +def test_413_returned_when_raise_for_status_disabled(ops_backend_413): + client = Client("key", "secret", middlewares=[], raise_for_status=False) + response = _issue_fulltext_request(client) + assert response.status_code == 413 + assert "CLIENT.AmbiguousRequest" in response.text + assert "publication/docdb/EP.0536425.B1" in response.text