Make a request with the provided data.

    @param url The url to make the request to.
    @param method The HTTP request method (defaulting to "GET").
    @param query A dict of the query key and values.
    @param body The optional body as a string or as a JSON decoded dict.
    @param auth The optional username and password as a tuple,
    not used if client is not None
    @param timeout The request timeout in seconds, defaulting to 10 seconds.
    @param client (httpbakery.Client) holds a context for making http
    requests with macaroons.
    @param macaroons Optional JSON serialized, base64 encoded macaroons to be
        included in the request header.

    POST/PUT request bodies are assumed to be in JSON format.
    Return the response content as a JSON decoded object, or an empty dict.
    Raise a ServerError if a problem occurs in the request/response process.
    Raise a ValueError if invalid parameters are provided.

def make_request(
        url, method='GET', query=None, body=None, auth=None, timeout=10,
        client=None, macaroons=None):
    """Make a request with the provided data.

    @param url The url to make the request to.
    @param method The HTTP request method (defaulting to "GET").
    @param query A dict of the query key and values.
    @param body The optional body as a string or as a JSON decoded dict.
    @param auth The optional username and password as a tuple,
    not used if client is not None
    @param timeout The request timeout in seconds, defaulting to 10 seconds.
    @param client (httpbakery.Client) holds a context for making http
    requests with macaroons.
    @param macaroons Optional JSON serialized, base64 encoded macaroons to be
        included in the request header.

    POST/PUT request bodies are assumed to be in JSON format.
    Return the response content as a JSON decoded object, or an empty dict.
    Raise a ServerError if a problem occurs in the request/response process.
    Raise a ValueError if invalid parameters are provided.
    """
    headers = {}
    kwargs = {'timeout': timeout, 'headers': headers}
    # Handle the request body.
    if body is not None:
        if isinstance(body, collections.Mapping):
            body = json.dumps(body)
        kwargs['data'] = body
    # Handle request methods.
    if method in ('GET', 'HEAD'):
        if query:
            url = '{}?{}'.format(url, urlencode(query, True))
    elif method in ('DELETE', 'PATCH', 'POST', 'PUT'):
        headers['Content-Type'] = 'application/json'
    else:
        raise ValueError('invalid method {}'.format(method))
    if macaroons is not None:
        headers['Macaroons'] = macaroons

    kwargs['auth'] = auth if client is None else client.auth()

    api_method = getattr(requests, method.lower())
    # Perform the request.
    try:
        response = api_method(url, **kwargs)
    except requests.exceptions.Timeout:
        raise timeout_error(url, timeout)
    except Exception as err:
        msg = _server_error_message(url, err)
        raise ServerError(msg)
    # Handle error responses.
    try:
        response.raise_for_status()
    except HTTPError as err:
        msg = _server_error_message(url, err.response.text)
        raise ServerError(err.response.status_code, msg)
    except requests.exceptions.RequestException as err:
        msg = _server_error_message(url, err.message)
        raise ServerError(msg)
    # Some requests just result in a status with no response body.
    if not response.content:
        return {}
    # Assume the response body is a JSON encoded string.
    try:
        return response.json()
    except Exception as err:
        msg = 'Error decoding JSON response: {} message: {}'.format(url, err)
        log.error(msg)
        raise ServerError(msg)