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_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_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_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_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 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 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 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