xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/restful.py (revision 959b34e9f9ee10168900b26e36c418e4b6056e05)
12d57dc69SVladimir Kotal#
22d57dc69SVladimir Kotal# CDDL HEADER START
32d57dc69SVladimir Kotal#
42d57dc69SVladimir Kotal# The contents of this file are subject to the terms of the
52d57dc69SVladimir Kotal# Common Development and Distribution License (the "License").
62d57dc69SVladimir Kotal# You may not use this file except in compliance with the License.
72d57dc69SVladimir Kotal#
82d57dc69SVladimir Kotal# See LICENSE.txt included in this distribution for the specific
92d57dc69SVladimir Kotal# language governing permissions and limitations under the License.
102d57dc69SVladimir Kotal#
112d57dc69SVladimir Kotal# When distributing Covered Code, include this CDDL HEADER in each
122d57dc69SVladimir Kotal# file and include the License file at LICENSE.txt.
132d57dc69SVladimir Kotal# If applicable, add the following below this CDDL HEADER, with the
142d57dc69SVladimir Kotal# fields enclosed by brackets "[]" replaced with your own identifying
152d57dc69SVladimir Kotal# information: Portions Copyright [yyyy] [name of copyright owner]
162d57dc69SVladimir Kotal#
172d57dc69SVladimir Kotal# CDDL HEADER END
182d57dc69SVladimir Kotal#
192d57dc69SVladimir Kotal
202d57dc69SVladimir Kotal#
21cc7022c6SVladimir Kotal# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved.
222d57dc69SVladimir Kotal#
232d57dc69SVladimir Kotal
242d57dc69SVladimir Kotalimport json
252d57dc69SVladimir Kotalimport logging
261c258122SVladimir Kotalimport time
272d57dc69SVladimir Kotal
282d57dc69SVladimir Kotalimport requests
292d57dc69SVladimir Kotal
302d57dc69SVladimir Kotalfrom .patterns import COMMAND_PROPERTY
312d57dc69SVladimir Kotalfrom .webutil import get_proxies
322d57dc69SVladimir Kotal
332d57dc69SVladimir KotalCONTENT_TYPE = 'Content-Type'
342d57dc69SVladimir KotalAPPLICATION_JSON = 'application/json'   # default
352d57dc69SVladimir Kotal
362d57dc69SVladimir Kotal
37*959b34e9SVladimir Kotaldef call_finished(location_uri, headers, timeout):
38*959b34e9SVladimir Kotal    """
39*959b34e9SVladimir Kotal    :param location_uri: URI to check the status of API call
40*959b34e9SVladimir Kotal    :param headers: HTTP headers
41*959b34e9SVladimir Kotal    :param timeout: connect timeout
42*959b34e9SVladimir Kotal    """
43*959b34e9SVladimir Kotal    logger = logging.getLogger(__name__)
44*959b34e9SVladimir Kotal
45*959b34e9SVladimir Kotal    logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}")
46*959b34e9SVladimir Kotal    response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
47*959b34e9SVladimir Kotal    if response is None:
48*959b34e9SVladimir Kotal        raise Exception("API call failed")
49*959b34e9SVladimir Kotal
50*959b34e9SVladimir Kotal    response.raise_for_status()
51*959b34e9SVladimir Kotal    if response.status_code == 202:
52*959b34e9SVladimir Kotal        return False
53*959b34e9SVladimir Kotal    else:
54*959b34e9SVladimir Kotal        return True
55*959b34e9SVladimir Kotal
56*959b34e9SVladimir Kotal
57*959b34e9SVladimir Kotaldef wait_for_async_api(response, api_timeout=None, headers=None, timeout=None):
581c258122SVladimir Kotal    """
591c258122SVladimir Kotal    :param response: request
60*959b34e9SVladimir Kotal    :param api_timeout: asynchronous API timeout (will wait forever or until error if None)
611c258122SVladimir Kotal    :param headers: request headers
621c258122SVladimir Kotal    :param timeout: connect timeout
631c258122SVladimir Kotal    :return: request
641c258122SVladimir Kotal    """
651c258122SVladimir Kotal    logger = logging.getLogger(__name__)
661c258122SVladimir Kotal
671c258122SVladimir Kotal    location_uri = response.headers.get("Location")
681c258122SVladimir Kotal    if location_uri is None:
691c258122SVladimir Kotal        raise Exception(f"no Location header in {response}")
701c258122SVladimir Kotal
71cc7022c6SVladimir Kotal    start_time = time.time()
72*959b34e9SVladimir Kotal    if api_timeout is None:
73*959b34e9SVladimir Kotal        while True:
74*959b34e9SVladimir Kotal            if call_finished(location_uri, headers, timeout):
75*959b34e9SVladimir Kotal                break
761c258122SVladimir Kotal            time.sleep(1)
771c258122SVladimir Kotal    else:
78*959b34e9SVladimir Kotal        for _ in range(api_timeout):
79*959b34e9SVladimir Kotal            if call_finished(location_uri, headers, timeout):
801c258122SVladimir Kotal                break
81*959b34e9SVladimir Kotal            time.sleep(1)
821c258122SVladimir Kotal
831c258122SVladimir Kotal    if response.status_code == 202:
84cc7022c6SVladimir Kotal        wait_time = time.time() - start_time
85cc7022c6SVladimir Kotal        logger.warn(f"API request still not completed after {int(wait_time)} seconds: {response}")
861c258122SVladimir Kotal        return response
871c258122SVladimir Kotal
881c258122SVladimir Kotal    logger.debug(f"DELETE API call to {location_uri}")
891c258122SVladimir Kotal    requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
901c258122SVladimir Kotal
911c258122SVladimir Kotal    return response
921c258122SVladimir Kotal
931c258122SVladimir Kotal
94f8f28195SVladimir Kotaldef do_api_call(verb, uri, params=None, headers=None, data=None, timeout=None, api_timeout=None):
952d57dc69SVladimir Kotal    """
962d57dc69SVladimir Kotal    Perform an API call. Will raise an exception if the request fails.
972d57dc69SVladimir Kotal    :param verb: string holding HTTP verb
982d57dc69SVladimir Kotal    :param uri: URI string
992d57dc69SVladimir Kotal    :param params: request parameters
1002d57dc69SVladimir Kotal    :param headers: HTTP headers dictionary
1012d57dc69SVladimir Kotal    :param data: data or None
102732be1c2SVladimir Kotal    :param timeout: optional connect timeout in seconds.
1031c258122SVladimir Kotal                    Applies also to asynchronous API status calls.
1041c258122SVladimir Kotal    :param api_timeout: optional timeout for asynchronous API requests in seconds.
1052d57dc69SVladimir Kotal    :return: the result of the handler call, can be None
1062d57dc69SVladimir Kotal    """
1072d57dc69SVladimir Kotal    logger = logging.getLogger(__name__)
1082d57dc69SVladimir Kotal
1092d57dc69SVladimir Kotal    handler = getattr(requests, verb.lower())
1102d57dc69SVladimir Kotal    if handler is None or not callable(handler):
1112d57dc69SVladimir Kotal        raise Exception('Unknown HTTP verb: {}'.format(verb))
1122d57dc69SVladimir Kotal
113cc7022c6SVladimir Kotal    logger.debug("{} API call: {} with data '{}', connect timeout {} seconds, API timeout {} seconds and headers: {}".
114cc7022c6SVladimir Kotal                 format(verb, uri, data, timeout, api_timeout, headers))
1152d57dc69SVladimir Kotal    r = handler(
1162d57dc69SVladimir Kotal        uri,
1172d57dc69SVladimir Kotal        data=data,
1182d57dc69SVladimir Kotal        params=params,
1192d57dc69SVladimir Kotal        headers=headers,
120732be1c2SVladimir Kotal        proxies=get_proxies(uri),
121732be1c2SVladimir Kotal        timeout=timeout
1222d57dc69SVladimir Kotal    )
1232d57dc69SVladimir Kotal
1242d57dc69SVladimir Kotal    if r is None:
1252d57dc69SVladimir Kotal        raise Exception("API call failed")
1262d57dc69SVladimir Kotal
1271c258122SVladimir Kotal    if r.status_code == 202:
128*959b34e9SVladimir Kotal        r = wait_for_async_api(r, api_timeout=api_timeout, headers=headers, timeout=timeout)
1291c258122SVladimir Kotal
1302d57dc69SVladimir Kotal    r.raise_for_status()
1312d57dc69SVladimir Kotal
1322d57dc69SVladimir Kotal    return r
1332d57dc69SVladimir Kotal
1342d57dc69SVladimir Kotal
1352d97c0a2SVladimir Kotaldef subst(src, substitutions):
1362d97c0a2SVladimir Kotal    if substitutions:
1372d97c0a2SVladimir Kotal        for pattern, value in substitutions.items():
1382d97c0a2SVladimir Kotal            if value:
1392d97c0a2SVladimir Kotal                src = src.replace(pattern, value)
1402d97c0a2SVladimir Kotal
1412d97c0a2SVladimir Kotal    return src
1422d97c0a2SVladimir Kotal
1432d97c0a2SVladimir Kotal
144732be1c2SVladimir Kotaldef call_rest_api(command, substitutions=None, http_headers=None, timeout=None):
1452d57dc69SVladimir Kotal    """
1462d57dc69SVladimir Kotal    Make RESTful API call. Occurrence of the pattern in the URI
1472d57dc69SVladimir Kotal    (first part of the command) or data payload will be replaced by the name.
1482d57dc69SVladimir Kotal
1492d57dc69SVladimir Kotal    Default content type is application/json.
1502d57dc69SVladimir Kotal
15189229afdSVladimir Kotal    :param command: command (list of URI, HTTP verb, data payload,
15289229afdSVladimir Kotal                             HTTP header dictionary)
1532d97c0a2SVladimir Kotal    :param substitutions: dictionary of pattern:value for command and/or
1542d97c0a2SVladimir Kotal                          data substitution
15589229afdSVladimir Kotal    :param http_headers: optional dictionary of HTTP headers to be appended
156732be1c2SVladimir Kotal    :param timeout: optional timeout in seconds for API call response
157*959b34e9SVladimir Kotal    :return value from given requests method
1582d57dc69SVladimir Kotal    """
1592d57dc69SVladimir Kotal
1602d57dc69SVladimir Kotal    logger = logging.getLogger(__name__)
1612d57dc69SVladimir Kotal
1622d57dc69SVladimir Kotal    if not isinstance(command, dict) or command.get(COMMAND_PROPERTY) is None:
1632d57dc69SVladimir Kotal        raise Exception("invalid command")
1642d57dc69SVladimir Kotal
1652d57dc69SVladimir Kotal    command = command[COMMAND_PROPERTY]
1662d57dc69SVladimir Kotal
1672d57dc69SVladimir Kotal    uri, verb, data, *_ = command
1682d57dc69SVladimir Kotal    try:
1692d57dc69SVladimir Kotal        headers = command[3]
1702d57dc69SVladimir Kotal        if headers and not isinstance(headers, dict):
1712d57dc69SVladimir Kotal            raise Exception("headers must be a dictionary")
1722d57dc69SVladimir Kotal    except IndexError:
1732d57dc69SVladimir Kotal        headers = {}
1742d57dc69SVladimir Kotal
1752d57dc69SVladimir Kotal    if headers is None:
1762d57dc69SVladimir Kotal        headers = {}
1772d57dc69SVladimir Kotal
178b369c884SVladimir Kotal    logger.debug("Headers from the command: {}".format(headers))
17989229afdSVladimir Kotal    if http_headers:
180b369c884SVladimir Kotal        logger.debug("Updating HTTP headers for command {} with {}".
181b369c884SVladimir Kotal                     format(command, http_headers))
18289229afdSVladimir Kotal        headers.update(http_headers)
18389229afdSVladimir Kotal
1842d97c0a2SVladimir Kotal    uri = subst(uri, substitutions)
1852d57dc69SVladimir Kotal    header_names = [x.lower() for x in headers.keys()]
1862d57dc69SVladimir Kotal
1872d57dc69SVladimir Kotal    if data:
1882d57dc69SVladimir Kotal        if CONTENT_TYPE.lower() not in header_names:
1892d57dc69SVladimir Kotal            logger.debug("Adding header: {} = {}".
1902d57dc69SVladimir Kotal                         format(CONTENT_TYPE, APPLICATION_JSON))
1912d57dc69SVladimir Kotal            headers[CONTENT_TYPE] = APPLICATION_JSON
1922d57dc69SVladimir Kotal
1932d57dc69SVladimir Kotal        for (k, v) in headers.items():
1942d57dc69SVladimir Kotal            if k.lower() == CONTENT_TYPE.lower():
1952d57dc69SVladimir Kotal                if headers[k].lower() == APPLICATION_JSON.lower():
1962d57dc69SVladimir Kotal                    logger.debug("Converting {} to JSON".format(data))
1972d57dc69SVladimir Kotal                    data = json.dumps(data)
1982d57dc69SVladimir Kotal                break
1992d57dc69SVladimir Kotal
2002d97c0a2SVladimir Kotal        data = subst(data, substitutions)
2012d57dc69SVladimir Kotal        logger.debug("entity data: {}".format(data))
2022d57dc69SVladimir Kotal
203732be1c2SVladimir Kotal    return do_api_call(verb, uri, headers=headers, data=data, timeout=timeout)
204