diff --git a/README.md b/README.md index a653dd2db5e470c943873fc4339570f5fed2b645..dc01b7605b6e6c04b6f0165265a42bbbaa07a40a 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ This can be useful to force evaluation of a `LazyPostgrestJsonResult`. Using a `LazyPostgrestJsonResult` in any boolean context will evaluate it. - Using the `singular=True` kwarg. Getting some lazy object when explicitly requesting a single element doesn't make much sense. -Like django's `Model.objects.get()` this will return the requested element or raise a HTTPError if none or multiple objects were found. +Like django's `Model.objects.get()` this will return the requested element or raise a `ObjectDoesNotExist`/`MultipleObjectsReturned` if none or multiple objects were found. #### Pagination diff --git a/postgrestutils/client/__init__.py b/postgrestutils/client/__init__.py index fce495c26395d3a5d710c7f29dfde11e55028fe6..88d9cb0d6c3b4b34ad08bd350db665b3d48602f6 100644 --- a/postgrestutils/client/__init__.py +++ b/postgrestutils/client/__init__.py @@ -1,7 +1,10 @@ from .. import app_settings -from .postgrestclient import Count, PostgrestClient +from .postgrestclient import ( + Count, MultipleObjectsReturned, ObjectDoesNotExist, PostgrestClient, +) # the instance of the client to be used pgrest_client = PostgrestClient(app_settings.BASE_URI, app_settings.JWT) -__all__ = ['Count', 'pgrest_client'] + +__all__ = ['Count', 'pgrest_client', 'ObjectDoesNotExist', 'MultipleObjectsReturned'] diff --git a/postgrestutils/client/postgrestclient.py b/postgrestutils/client/postgrestclient.py index 9134dc263ccb241194915219578f4902c2e123a9..575c5f121f55c5a88b8cf57363746d88cf4cb7f8 100644 --- a/postgrestutils/client/postgrestclient.py +++ b/postgrestutils/client/postgrestclient.py @@ -1,9 +1,11 @@ import copy import enum +import re from urllib.parse import urljoin import requests +from postgrestutils import logger from postgrestutils.client.utils import datetime_parser REPR_OUTPUT_SIZE = 20 @@ -11,6 +13,14 @@ REPR_OUTPUT_SIZE = 20 Count = enum.Enum('Count', (('NONE', None), ('EXACT', 'exact'))) +class ObjectDoesNotExist(Exception): + pass + + +class MultipleObjectsReturned(Exception): + pass + + class PostgrestClient: def __init__(self, base_uri, token=None): self.session = requests.Session() @@ -32,8 +42,9 @@ class PostgrestClient: PostgREST to python datetime objects :param count: counting strategy as explained in the README :param kwargs: pass kwargs directly to requests's .get() method - :return: single element as dict (singular=True), lazy python object for - multiple elements or raises HTTPError + :return: single element as dict + :raises: ObjectDoesNotExist/MultipleObjectsReturned if no or more than + one object was found """ if singular: # immediately evaluate and return result res = LazyPostgrestJsonResult(self, endpoint, singular, parse_dt, count, **kwargs) @@ -117,7 +128,15 @@ class LazyPostgrestJsonResult: try: resp.raise_for_status() except requests.HTTPError as e: - raise type(e)(resp.status_code, resp.reason, resp.text) + # try getting a more detailed exception if status_code = 406 + if resp.status_code == 406: + try: + self._try_parse_406(resp) + except (ObjectDoesNotExist, MultipleObjectsReturned) as detailed: + raise detailed from e + + # fall back to raising a generic HTTPError exception + raise type(e)(resp.status_code, resp.reason, resp.text, response=resp, request=e.request) if self.parse_dt: json_result = resp.json(object_hook=datetime_parser) @@ -126,6 +145,30 @@ class LazyPostgrestJsonResult: # always return a list even if it contains a single element only return [json_result] if self.singular else json_result + def _try_parse_406(self, resp): + """ + Try parsing a 406 HTTPError to raise a more detailed error message. + :param resp: the HTTP response to parse + """ + detail_regex = re.compile( + r'Results contain (?P<row_count>\d+) rows, application/vnd\.pgrst\.object\+json requires 1 row' + ) + try: + json = resp.json() + except ValueError: + # Failed parsing as json, give up trying to guess the error. + # PostgREST probably changed the error format, log the response for + # more insights. + logger.warning("Unparsable 406: {}".format(resp.text)) + else: + result = re.match(detail_regex, json['details']) + if result is not None: + row_count = int(result.group('row_count')) + if row_count == 0: + raise ObjectDoesNotExist(json) + else: + raise MultipleObjectsReturned(json) + def __getitem__(self, key): if not isinstance(key, (int, slice)): raise TypeError( diff --git a/setup.py b/setup.py index 59bb390a2a9be2bb2f67da715b2fc1988feaf9da..768497045e29059ea1f50848e876cc1482fe5930 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='postgrestutils', - version='0.1.0', + version='1.0.0', packages=find_packages(), include_package_data=True, license='BSD',