Skip to content

Commit

Permalink
reasonably helpful error responses
Browse files Browse the repository at this point in the history
  • Loading branch information
aaxelb committed May 31, 2024
1 parent 7694148 commit f8729f6
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 80 deletions.
63 changes: 59 additions & 4 deletions trove/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import http
import inspect


class TroveError(Exception):
pass
# set more helpful codes in subclasses
http_status: int = http.HTTPStatus.INTERNAL_SERVER_ERROR
error_location: str = ''

def __init__(self, *args):
super().__init__(*args)
self.error_location = _get_nearest_code_location()


###
Expand All @@ -13,11 +23,15 @@ class CannotDigestMediatype(DigestiveError):
pass


class CannotDigestDateValue(DigestiveError):
pass


###
# parsing a request

class RequestParsingError(TroveError):
pass
http_status = http.HTTPStatus.BAD_REQUEST


class InvalidQuotedIri(RequestParsingError):
Expand All @@ -28,10 +42,41 @@ class InvalidQueryParamName(RequestParsingError):
pass


class InvalidDate(RequestParsingError):
class InvalidFilterOperator(InvalidQueryParamName):
pass


class InvalidQueryParamValue(RequestParsingError):
pass


class InvalidSearchText(InvalidQueryParamValue):
pass


class MissingRequiredQueryParam(RequestParsingError):
pass


class InvalidRepeatedQueryParam(RequestParsingError):
pass


class InvalidPropertyPath(RequestParsingError):
pass


###
# rendering a response

class ResponseRenderingError(TroveError):
pass


class CannotRenderMediatype(ResponseRenderingError):
http_status = http.HTTPStatus.NOT_ACCEPTABLE


###
# primitive rdf

Expand Down Expand Up @@ -72,4 +117,14 @@ class OwlObjection(PrimitiveRdfWhoopsy):


###
# rendering a response
# local helpers

def _get_nearest_code_location() -> str:
try:
_raise_frame = next(
_frameinfo for _frameinfo in inspect.stack()
if _frameinfo.filename != __file__ # nearest frame not in this file
)
return f'{_raise_frame.filename}::{_raise_frame.lineno}'
except Exception:
return 'unknown' # eh, whatever
2 changes: 1 addition & 1 deletion trove/extract/legacy_sharev2.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def _date_or_none(maybe_date) -> typing.Optional[datetime.date]:
return maybe_date
if maybe_date is None:
return None
raise trove_exceptions.InvalidDate(maybe_date)
raise trove_exceptions.CannotDigestDateValue(maybe_date)


def _focustype_iris(mnode: MutableNode) -> typing.Iterable[str]:
Expand Down
2 changes: 1 addition & 1 deletion trove/models/indexcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,5 +307,5 @@ def deriver_cls(self):
def as_rdf_literal(self) -> rdf.Literal:
return rdf.literal(
self.derived_text,
self.deriver_cls.derived_datatype_iris(),
datatype_iris=self.deriver_cls.derived_datatype_iris(),
)
19 changes: 12 additions & 7 deletions trove/render/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
from trove import exceptions as trove_exceptions
from trove.vocab.trove import TROVE_API_THESAURUS
from trove.vocab.namespaces import NAMESPACES_SHORTHAND
from ._base import BaseRenderer
from .jsonapi import RdfJsonapiRenderer
from .html_browse import RdfHtmlBrowseRenderer
from .turtle import RdfTurtleRenderer
from .jsonld import RdfJsonldRenderer
from .simple_json import TrovesearchSimpleJsonRenderer


__all__ = ('get_renderer',)

RENDERERS: tuple[type[BaseRenderer], ...] = (
RdfHtmlBrowseRenderer,
RdfJsonapiRenderer,
RdfTurtleRenderer,
RdfJsonldRenderer,
TrovesearchSimpleJsonRenderer,
)

RENDERER_BY_MEDIATYPE = {
_renderer_cls.MEDIATYPE: _renderer_cls
for _renderer_cls in (
RdfHtmlBrowseRenderer,
RdfJsonapiRenderer,
RdfTurtleRenderer,
RdfJsonldRenderer,
TrovesearchSimpleJsonRenderer,
)
for _renderer_cls in RENDERERS
}
DEFAULT_RENDERER = RdfJsonapiRenderer # the most stable one

Expand Down
30 changes: 29 additions & 1 deletion trove/render/_base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import abc
import json
from typing import Optional, ClassVar

from django import http
from primitive_metadata import primitive_rdf as rdf

from trove import exceptions as trove_exceptions
from trove.vocab import mediatypes


class BaseRenderer(abc.ABC):
MEDIATYPE: ClassVar[str] # required in subclasses
# required in subclasses
MEDIATYPE: ClassVar[str]
# should be set when render_error_document is overridden:
ERROR_MEDIATYPE: ClassVar[str] = mediatypes.JSONAPI
# should be set when the renderer expects a specific derived metadata format
INDEXCARD_DERIVER_IRI: ClassVar[str | None] = None

def __init__(
self, *,
Expand All @@ -30,6 +39,25 @@ def render_response(
**response_kwargs,
)

def render_error_response(self, error: trove_exceptions.TroveError):
return http.HttpResponse(
content=self.render_error_document(error),
content_type=self.ERROR_MEDIATYPE,
status=error.http_status,
)

@abc.abstractmethod
def render_document(self, data: rdf.RdfGraph, focus_iri: str) -> str:
raise NotImplementedError

def render_error_document(self, error: trove_exceptions.TroveError) -> str:
# may override, but default to jsonapi
return json.dumps(
{'errors': [{ # https://jsonapi.org/format/#error-objects
'status': error.http_status,
'code': error.error_location,
'title': error.__class__.__name__,
'detail': str(error),
}]},
indent=2,
)
1 change: 1 addition & 0 deletions trove/render/jsonapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class RdfJsonapiRenderer(BaseRenderer):
note: does not support relationship links (or many other jsonapi features)
'''
MEDIATYPE = mediatypes.JSONAPI
INDEXCARD_DERIVER_IRI = TROVE['derive/osfmap_json']

__to_include: set[primitive_rdf.RdfObject] | None = None

Expand Down
3 changes: 2 additions & 1 deletion trove/render/jsonld.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from primitive_metadata import primitive_rdf as rdf

from trove import exceptions as trove_exceptions
from trove.vocab.namespaces import RDF, OWL
from trove.vocab.namespaces import RDF, OWL, TROVE
from trove.vocab import mediatypes
from ._base import BaseRenderer

Expand All @@ -18,6 +18,7 @@

class RdfJsonldRenderer(BaseRenderer):
MEDIATYPE = mediatypes.JSONLD
INDEXCARD_DERIVER_IRI = TROVE['derive/osfmap_json']

__visiting_iris: set | None = None

Expand Down
1 change: 1 addition & 0 deletions trove/render/simple_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class TrovesearchSimpleJsonRenderer(BaseRenderer):
'''for "simple json" search api -- very entangled with trove/trovesearch/trovesearch_gathering.py
'''
MEDIATYPE = mediatypes.JSON
INDEXCARD_DERIVER_IRI = TROVE['derive/osfmap_json']

def render_document(self, data: rdf.RdfGraph, focus_iri: str) -> str:
_focustypes = set(data.q(focus_iri, RDF.type))
Expand Down
2 changes: 2 additions & 0 deletions trove/render/turtle.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from primitive_metadata import primitive_rdf as rdf

from trove.vocab.namespaces import TROVE
from ._base import BaseRenderer


class RdfTurtleRenderer(BaseRenderer):
MEDIATYPE = 'text/turtle'
INDEXCARD_DERIVER_IRI = TROVE['derive/osfmap_json']

def render_document(self, rdf_graph: rdf.RdfGraph, focus_iri: str):
return rdf.turtle_from_tripledict(rdf_graph.tripledict, focus=focus_iri)
27 changes: 18 additions & 9 deletions trove/trovesearch/search_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _gather_shorthand(cls, queryparams: QueryparamDict):
try:
(_shortname,) = _qp_name.bracketed_names
except ValueError:
raise # TODO: 400 response
raise trove_exceptions.InvalidQueryParamName(_qp_name)
else:
_prefixmap[_shortname] = _iri
return NAMESPACES_SHORTHAND.with_update(_prefixmap)
Expand All @@ -119,7 +119,7 @@ class Textsegment:

def __post_init__(self):
if self.is_negated and self.is_fuzzy:
raise ValueError(f'{self}: cannot have both is_negated and is_fuzzy')
raise trove_exceptions.InvalidSearchText(self.text, "search cannot be both negated and fuzzy")

def words(self):
return self.text.split()
Expand Down Expand Up @@ -310,7 +310,10 @@ def from_filter_param(cls, param_name: QueryparamName, param_value: str):
try:
_operator = SearchFilter.FilterOperator.from_shortname(_operator_value)
except ValueError:
raise ValueError(f'unrecognized search-filter operator "{_operator_value}"')
raise trove_exceptions.InvalidQueryParamName(
str(param_name),
f'unknown filter operator "{_operator_value}"',
)
_propertypath_set = _parse_propertypath_set(_serialized_path_set)
_is_date_filter = all(
is_date_property(_path[-1])
Expand All @@ -323,7 +326,10 @@ def from_filter_param(cls, param_name: QueryparamName, param_value: str):
else SearchFilter.FilterOperator.ANY_OF
)
if _operator.is_date_operator() and not _is_date_filter:
raise ValueError(f'cannot use date operator {_operator.value} on non-date property')
raise trove_exceptions.InvalidQueryParamName(
str(param_name),
f'cannot use date operator "{_operator.to_shortname()}" on non-date property'
)
_value_list = []
if not _operator.is_valueless_operator():
for _value in split_queryparam_value(param_value):
Expand Down Expand Up @@ -390,7 +396,7 @@ def _from_sort_param_str(cls, param_value: str) -> typing.Iterable['SortParam']:
_sort_property = _sort.lstrip(DESCENDING_SORT_PREFIX)
_property_iri = osfmap_shorthand().expand_iri(_sort_property)
if not is_date_property(_property_iri):
raise ValueError(f'bad sort: {_sort_property}') # TODO: nice response
raise trove_exceptions.InvalidQueryParamValue('sort', _sort_property, "may not sort on non-date properties")
yield cls(
property_iri=_property_iri,
descending=param_value.startswith(DESCENDING_SORT_PREFIX),
Expand Down Expand Up @@ -476,7 +482,7 @@ class ValuesearchParams(CardsearchParams):
def parse_queryparams(cls, queryparams: QueryparamDict) -> dict:
_raw_propertypath = _get_single_value(queryparams, QueryparamName('valueSearchPropertyPath'))
if not _raw_propertypath:
raise ValueError('TODO: 400 valueSearchPropertyPath required')
raise trove_exceptions.MissingRequiredQueryParam('valueSearchPropertyPath')
return {
**super().parse_queryparams(queryparams),
'valuesearch_propertypath_set': _parse_propertypath_set(_raw_propertypath, allow_globs=False),
Expand Down Expand Up @@ -554,7 +560,7 @@ def _get_single_value(
try:
(_singlevalue,) = _paramvalues
except ValueError:
raise ValueError(f'expected at most one {queryparam_name} value, got {len(_paramvalues)}')
raise trove_exceptions.InvalidRepeatedQueryParam(str(queryparam_name))
else:
return _singlevalue

Expand All @@ -574,9 +580,12 @@ def _parse_propertypath(serialized_path: str, *, allow_globs=True) -> tuple[str,
)
if GLOB_PATHSTEP in _path:
if not allow_globs:
raise ValueError(f'no * allowed (got {serialized_path})')
raise trove_exceptions.InvalidPropertyPath(serialized_path, 'no * allowed')
if any(_pathstep != GLOB_PATHSTEP for _pathstep in _path):
raise ValueError(f'path must be all * or no * (got {serialized_path})')
raise trove_exceptions.InvalidPropertyPath(
serialized_path,
f'path must be all * or no * (got {serialized_path})',
)
return _path


Expand Down
Loading

0 comments on commit f8729f6

Please sign in to comment.