def test_parse_simple_subquery_fail(): """Test a simple subquery fails with invalid input.""" query_params = {"tracks._subquery_": 5} parser = ModelQueryParamParser(query_params) with raises(FilterParseError) as excinfo: parser.parse_subfilters() assert excinfo.value.code == "invalid_complex_filters"
def test_suboffset_parser_bad_value_fail(): """Ensure basic suboffset parsing fails appropriately.""" query_params = {"album.tracks._offset_": "test"} parser = ModelQueryParamParser(query_params) with raises(OffsetLimitParseError) as excinfo: parser.parse_subfilters() assert excinfo.value.code == "invalid_suboffset_value"
def test_invalid_subresource_path_fail(): """Ensure a bad subresource path fails.""" query_params = {"album.tracks._sorts_.failhere": "track_id,-name"} parser = ModelQueryParamParser(query_params) with raises(ParseError) as excinfo: parser.parse_subfilters() assert excinfo.value.code == "invalid_subresource_path"
def test_parse_complex_json_non_dict_fail(): """Ensure non dictionary json complex filters fail.""" query_params = {"query": "[]"} parser = ModelQueryParamParser(query_params) with raises(FilterParseError) as excinfo: parser.parse_filters(Album) assert excinfo.value.code == "invalid_complex_filters"
def test_subfilter_parser(): """Ensure basic subfilter parsing works.""" query_params = {"album.tracks._subquery_": '{"track_id": 5}'} parser = ModelQueryParamParser(query_params) result = parser.parse_subfilters() expected_result = {"$and": [{"track_id": 5}]} assert expected_result == result["album.tracks"].filters
def test_parse_simple_subquery(): """Test a simple subquery is handled properly.""" query_params = {"tracks._subquery_.playlists.playlist_id": 5} parser = ModelQueryParamParser(query_params) result = parser.parse_subfilters() filters = result["tracks"].filters assert filters["$and"][0]["playlists.playlist_id"]["$eq"] == 5
def test_parse_complex_subquery(): """Test a complex subquery is handled properly.""" query_params = { "tracks._subquery_.playlists": json.dumps({"playlist_id": 5}) } parser = ModelQueryParamParser(query_params) result = parser.parse_subfilters() filters = result["tracks"].filters assert filters["$and"][0]["playlists"]["playlist_id"] == 5
def test_parse_filters_ignore_subresource(): """Ensure filter parsing ignores any subresource paths.""" query_params = { "query": json.dumps({"title": "Big Ones"}), "tracks._sorts_": "name" } parser = ModelQueryParamParser(query_params) result = parser.parse_filters(Album) assert result["$and"][0]["title"] == "Big Ones"
def test_get_resources_ordered(db_session): """Test simple get_resources sort functionality.""" query_params = {"sort": "-album_id,title"} parser = ModelQueryParamParser(query_params) album_resource = AlbumResource(session=db_session) result = album_resource.get_collection(filters=parser.parse_filters( album_resource.model), sorts=parser.parse_sorts()) assert len(result) == 347 assert result[0]["album_id"] == 347
def test_subsorts_parser(): """Ensure basic subsorts parsing works.""" query_params = {"album.tracks._sorts_": "track_id,-name"} parser = ModelQueryParamParser(query_params) result = parser.parse_subfilters() assert len(result["album.tracks"].sorts) == 2 assert result["album.tracks"].sorts[0].attr == "track_id" assert result["album.tracks"].sorts[0].direction == "ASC" assert result["album.tracks"].sorts[1].attr == "name" assert result["album.tracks"].sorts[1].direction == "DESC"
def test_parse_multiple_complex_filters(): """Ensure multiple complex filters are treated properly.""" query_params = { "query": [json.dumps({"title": "Big Ones"}), json.dumps({"title": "Big Ones"})] } parser = ModelQueryParamParser(query_params) result = parser.parse_filters(Album) assert result["$and"][0]["title"] == "Big Ones" assert result["$and"][1]["title"] == "Big Ones"
def test_limit(db_session): """Make sure providing a limit query_param works.""" query_params = {"limit": "1"} parser = ModelQueryParamParser(query_params) album_resource = AlbumResource(session=db_session) offset_limit_info = parser.parse_offset_limit(page_max_size=30) offset = offset_limit_info.offset limit = offset_limit_info.limit result = album_resource.get_collection(filters=parser.parse_filters( album_resource.model), sorts=parser.parse_sorts(), limit=limit, offset=offset) assert len(result) == 1
def test_get_second_page(db_session): """Test that we can get the second page of a set of objects.""" query_params = {"sort": "album_id", "page": "2"} parser = ModelQueryParamParser(query_params) album_resource = AlbumResource(session=db_session) offset_limit_info = parser.parse_offset_limit(page_max_size=30) offset = offset_limit_info.offset limit = offset_limit_info.limit result = album_resource.get_collection(filters=parser.parse_filters( album_resource.model), sorts=parser.parse_sorts(), limit=limit, offset=offset) assert len(result) == 30 assert result[0]["album_id"] == 31
def test_subresource_nested_query(db_session): """Ensure a simple subresource query works.""" query_params = { "tracks._subquery_.track_id-gte": 5, "tracks.playlists._subquery_.playlist_id-lte": 5 } parser = ModelQueryParamParser(query_params) album_resource = AlbumResource(session=db_session) result = album_resource.get_collection( subfilters=parser.parse_subfilters(), embeds=parser.parse_embeds()) success = False for album in result: if album["album_id"] == 3: assert len(album["tracks"]) == 1 assert album["tracks"][0]["track_id"] == 5 success = True assert success
def test_parse_filters_convert_key_names(): """Ensure parsing filters works with key name conversion.""" def convert_key_names(key): if key == "titleTest": return "title" raise AttributeError query_params = {"titleTest": "Big Ones", "badkey": "test"} parser = ModelQueryParamParser(query_params) result = parser.parse_filters(Album, convert_key_names_func=convert_key_names) # Note that this is still titleTest # convert_key_names job here is only to be used # to verify that an attribute exists in the # provided model. Since titleTest is converted # to title, and title is an attribute in Album, # titleTest is part of the query, unlike badkey. assert result["$and"][0]["titleTest"]["$eq"] == "Big Ones"
def test_sublimit_parser_bad_value_ignore(): """Ensure non strict basic sublimit parsing ignores errors.""" query_params = {"album.tracks._limit_": "test", "album.tracks._offset_": 5} parser = ModelQueryParamParser(query_params) result = parser.parse_subfilters(strict=False) assert result["album.tracks"].offset == 5
def test_invalid_subresource_path_ignore(): """Ensure silent failure on subresource path when not strict.""" query_params = {"album.tracks._sorts_.failhere": "track_id,-name"} parser = ModelQueryParamParser(query_params) result = parser.parse_subfilters(strict=False) assert len(result) == 0
def test_root_complex_filters_parser(): """Ensure basic root complex parsing works.""" query_params = {"query": json.dumps({"title": "Big Ones"})} parser = ModelQueryParamParser(query_params) result = parser.parse_filters(Album) assert result["$and"][0]["title"] == "Big Ones"
def delete(self, path, query_params=None): """Generic API router for DELETE requests. :param str path: The resource path specified. This should not include the root ``/api`` or any versioning info. :param query_params: Dictionary of query parameters, likely provided as part of a request. Defaults to an empty dict. :type query_params: dict or None :return: ``None`` if successful. :raise ResourceNotFoundError: If no resource can be found at the provided path. :raise BadRequestError: Invalid filters, sorts, fields, embeds, offset, or limit as defined in the provided query params will result in a raised exception if strict is set to ``True``. :raise MethodNotAllowedError: If deleting the resource at the supplied path is not allowed. """ if self.resource is None: self._deduce_resource(path) path_objs = self._get_path_objects(path) resource = path_objs.get("resource", None) parent_resource = path_objs.get("parent_resource", None) path_part = path_objs.get("path_part", None) query_session = path_objs.get("query_session", None) ident = path_objs.get("ident", None) parser = ModelQueryParamParser(query_params, context=self.context) # last path_part determines what type of request this is if isinstance(path_part, Field) and not isinstance( path_part, NestedPermissibleABC): # Simple property, such as album_id # set the value field_name = path_part.data_key or path_part.name data = {field_name: None} result = resource.patch(ident=ident, data=data) if result is not None and field_name in result: return result[field_name] # failsafe, should be caught by _get_path_objects raise self.make_error("resource_not_found", path=path) # pragma: no cover elif isinstance(path_part, NestedPermissibleABC): # subresource # Delete contents of the relationship if path_part.many: return self._subfield_update(method="delete", data=[], parent_resource=parent_resource, resource=resource, path_part=path_part, ident=ident, path=path) else: return self._subfield_update(method="put", data=None, parent_resource=parent_resource, resource=resource, path_part=path_part, ident=ident, path=path) elif isinstance(path_part, BaseModelResource): # resource collection # any subresource field would already have been handled filters = parser.parse_filters( resource.model, convert_key_names_func=resource.convert_key_name) return resource.delete_collection(filters=filters, session=query_session) elif isinstance(path_part, tuple): # path part is a resource identifier # individual instance return resource.delete(ident=path_part) raise self.make_error("resource_not_found", path=path) # pragma: no cover
def test_invalid_complex_subfilters(): """Test that bad complex filters fail properly.""" parser = ModelQueryParamParser(query_params={"tracks._subfilter_": "{"}) with raises(FilterParseError) as excinfo: parser.parse_filters(Album) assert excinfo.value.code == "invalid_complex_filters"
def test_missing_error_message_fail(): """Test that failing with a bad error message is handled.""" parser = ModelQueryParamParser(query_params={}) with raises(AssertionError): parser.make_error(key="test")
def test_suboffset_parser(): """Ensure basic suboffset parsing works.""" query_params = {"album.tracks._offset_": 5} parser = ModelQueryParamParser(query_params) result = parser.parse_subfilters() assert result["album.tracks"].offset == 5
def get(self, path, query_params=None, strict=True, head=False): """Generic API router for GET requests. :param str path: The resource path specified. This should not include the root ``/api`` or any versioning info. :param query_params: Dictionary of query parameters, likely provided as part of a request. Defaults to an empty dict. :type query_params: dict or None :param bool strict: If ``True``, bad query params will raise non fatal errors rather than ignoring them. :param bool head: ``True`` if this was a HEAD request. :return: If this is a single entity query, an individual resource or ``None``. If this is a collection query, a list of resources. If it's an instance field query, the raw field value. :raise ResourceNotFoundError: If no resource can be found at the provided path. :raise BadRequestError: Invalid filters, sorts, fields, embeds, offset, or limit as defined in the provided query params will result in a raised exception if strict is set to ``True``. """ if self.resource is None: self._deduce_resource(path) path_objs = self._get_path_objects(path) resource = path_objs.get("resource", None) path_part = path_objs.get("path_part", None) query_session = path_objs.get("query_session", None) ident = path_objs.get("ident", None) parser = ModelQueryParamParser(query_params, context=self.context) fields = parser.parse_fields() embeds = parser.parse_embeds() try: subfilters = parser.parse_subfilters(strict=strict) except ParseError as exc: if strict: raise BadRequestError(code=exc.code, message=exc.message, **exc.kwargs) subfilters = None # last path_part determines what type of request this is if isinstance(path_part, Field) and not isinstance( path_part, NestedPermissibleABC): # Simple property, such as album_id # return only the value field_name = path_part.data_key or path_part.name result = resource.get(ident=ident, fields=[field_name], strict=strict, session=query_session, head=head) if result is not None and field_name in result: return result[field_name] raise self.make_error("resource_not_found", path=path) # pragma: no cover if isinstance(path_part, Field) or isinstance(path_part, BaseModelResource): # resource collection # any non subresource field would already have been handled try: filters = parser.parse_filters( resource.model, convert_key_names_func=resource.convert_key_name) except FilterParseError as e: if strict: raise BadRequestError(code=e.code, message=e.message, **e.kwargs) filters = None if not (isinstance(path_part, Nested) and not path_part.many): try: offset_limit_info = parser.parse_offset_limit( resource.page_max_size) offset = offset_limit_info.offset limit = offset_limit_info.limit except OffsetLimitParseError as e: if strict: raise BadRequestError(code=e.code, message=e.message, **e.kwargs) offset, limit = None, None sorts = parser.parse_sorts() results = resource.get_collection(filters=filters, subfilters=subfilters, fields=fields, embeds=embeds, sorts=sorts, offset=offset, limit=limit, session=query_session, strict=strict, head=head) if query_params.get("page") is not None or not offset: results.current_page = int(query_params.get("page") or 1) results.page_size = limit or resource.page_max_size return results else: result = resource.get_collection(fields=fields, embeds=embeds, subfilters=subfilters, session=query_session, strict=strict, head=head) if len(result) != 1: # pragma: no cover # failsafe, _get_path_objects will catch this first. raise self.make_error("resource_not_found", path=path) return result[0] elif isinstance(path_part, tuple): # path part is a resource identifier # individual instance return resource.get(ident=path_part, fields=fields, embeds=embeds, subfilters=subfilters, strict=strict, session=query_session, head=head) raise self.make_error("resource_not_found", path=path) # pragma: no cover