xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/restful.py (revision 1c258122d17e56599843d69a69f1445a01e0cd68)
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#
212d97c0a2SVladimir Kotal# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved.
222d57dc69SVladimir Kotal#
232d57dc69SVladimir Kotal
242d57dc69SVladimir Kotalimport json
252d57dc69SVladimir Kotalimport logging
26*1c258122SVladimir 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*1c258122SVladimir Kotaldef wait_for_async_api(response, api_timeout, headers=None, timeout=None):
38*1c258122SVladimir Kotal    """
39*1c258122SVladimir Kotal    :param response: request
40*1c258122SVladimir Kotal    :param api_timeout: asynchronous API timeout
41*1c258122SVladimir Kotal    :param headers: request headers
42*1c258122SVladimir Kotal    :param timeout: connect timeout
43*1c258122SVladimir Kotal    :return: request
44*1c258122SVladimir Kotal    """
45*1c258122SVladimir Kotal    logger = logging.getLogger(__name__)
46*1c258122SVladimir Kotal
47*1c258122SVladimir Kotal    location_uri = response.headers.get("Location")
48*1c258122SVladimir Kotal    if location_uri is None:
49*1c258122SVladimir Kotal        raise Exception(f"no Location header in {response}")
50*1c258122SVladimir Kotal
51*1c258122SVladimir Kotal    for _ in range(api_timeout):
52*1c258122SVladimir Kotal        logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}")
53*1c258122SVladimir Kotal        response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
54*1c258122SVladimir Kotal        if response is None:
55*1c258122SVladimir Kotal            raise Exception("API call failed")
56*1c258122SVladimir Kotal
57*1c258122SVladimir Kotal        if response.status_code == 202:
58*1c258122SVladimir Kotal            time.sleep(1)
59*1c258122SVladimir Kotal        else:
60*1c258122SVladimir Kotal            break
61*1c258122SVladimir Kotal
62*1c258122SVladimir Kotal    if response.status_code == 202:
63*1c258122SVladimir Kotal        logger.warn(f"API request still not completed: {response}")
64*1c258122SVladimir Kotal        return response
65*1c258122SVladimir Kotal
66*1c258122SVladimir Kotal    logger.debug(f"DELETE API call to {location_uri}")
67*1c258122SVladimir Kotal    requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
68*1c258122SVladimir Kotal
69*1c258122SVladimir Kotal    return response
70*1c258122SVladimir Kotal
71*1c258122SVladimir Kotal
72*1c258122SVladimir Kotaldef do_api_call(verb, uri, params=None, headers=None, data=None, timeout=None, api_timeout=None):
732d57dc69SVladimir Kotal    """
742d57dc69SVladimir Kotal    Perform an API call. Will raise an exception if the request fails.
752d57dc69SVladimir Kotal    :param verb: string holding HTTP verb
762d57dc69SVladimir Kotal    :param uri: URI string
772d57dc69SVladimir Kotal    :param params: request parameters
782d57dc69SVladimir Kotal    :param headers: HTTP headers dictionary
792d57dc69SVladimir Kotal    :param data: data or None
80732be1c2SVladimir Kotal    :param timeout: optional connect timeout in seconds.
81*1c258122SVladimir Kotal                    Applies also to asynchronous API status calls.
82732be1c2SVladimir Kotal                    If None, default (60 seconds) will be used.
83*1c258122SVladimir Kotal    :param api_timeout: optional timeout for asynchronous API requests in seconds.
84*1c258122SVladimir Kotal                    If None, default (300 seconds) will be used.
852d57dc69SVladimir Kotal    :return: the result of the handler call, can be None
862d57dc69SVladimir Kotal    """
872d57dc69SVladimir Kotal    logger = logging.getLogger(__name__)
882d57dc69SVladimir Kotal
892d57dc69SVladimir Kotal    handler = getattr(requests, verb.lower())
902d57dc69SVladimir Kotal    if handler is None or not callable(handler):
912d57dc69SVladimir Kotal        raise Exception('Unknown HTTP verb: {}'.format(verb))
922d57dc69SVladimir Kotal
93732be1c2SVladimir Kotal    if timeout is None:
94732be1c2SVladimir Kotal        timeout = 60
95732be1c2SVladimir Kotal
96*1c258122SVladimir Kotal    if api_timeout is None:
97*1c258122SVladimir Kotal        api_timeout = 300
98*1c258122SVladimir Kotal
99732be1c2SVladimir Kotal    logger.debug("{} API call: {} with data '{}', timeout {} seconds and headers: {}".
100732be1c2SVladimir Kotal                 format(verb, uri, data, timeout, headers))
1012d57dc69SVladimir Kotal    r = handler(
1022d57dc69SVladimir Kotal        uri,
1032d57dc69SVladimir Kotal        data=data,
1042d57dc69SVladimir Kotal        params=params,
1052d57dc69SVladimir Kotal        headers=headers,
106732be1c2SVladimir Kotal        proxies=get_proxies(uri),
107732be1c2SVladimir Kotal        timeout=timeout
1082d57dc69SVladimir Kotal    )
1092d57dc69SVladimir Kotal
1102d57dc69SVladimir Kotal    if r is None:
1112d57dc69SVladimir Kotal        raise Exception("API call failed")
1122d57dc69SVladimir Kotal
113*1c258122SVladimir Kotal    if r.status_code == 202:
114*1c258122SVladimir Kotal        r = wait_for_async_api(r, api_timeout, headers=headers, timeout=timeout)
115*1c258122SVladimir Kotal
1162d57dc69SVladimir Kotal    r.raise_for_status()
1172d57dc69SVladimir Kotal
1182d57dc69SVladimir Kotal    return r
1192d57dc69SVladimir Kotal
1202d57dc69SVladimir Kotal
1212d97c0a2SVladimir Kotaldef subst(src, substitutions):
1222d97c0a2SVladimir Kotal    if substitutions:
1232d97c0a2SVladimir Kotal        for pattern, value in substitutions.items():
1242d97c0a2SVladimir Kotal            if value:
1252d97c0a2SVladimir Kotal                src = src.replace(pattern, value)
1262d97c0a2SVladimir Kotal
1272d97c0a2SVladimir Kotal    return src
1282d97c0a2SVladimir Kotal
1292d97c0a2SVladimir Kotal
130732be1c2SVladimir Kotaldef call_rest_api(command, substitutions=None, http_headers=None, timeout=None):
1312d57dc69SVladimir Kotal    """
1322d57dc69SVladimir Kotal    Make RESTful API call. Occurrence of the pattern in the URI
1332d57dc69SVladimir Kotal    (first part of the command) or data payload will be replaced by the name.
1342d57dc69SVladimir Kotal
1352d57dc69SVladimir Kotal    Default content type is application/json.
1362d57dc69SVladimir Kotal
13789229afdSVladimir Kotal    :param command: command (list of URI, HTTP verb, data payload,
13889229afdSVladimir Kotal                             HTTP header dictionary)
1392d97c0a2SVladimir Kotal    :param substitutions: dictionary of pattern:value for command and/or
1402d97c0a2SVladimir Kotal                          data substitution
14189229afdSVladimir Kotal    :param http_headers: optional dictionary of HTTP headers to be appended
142732be1c2SVladimir Kotal    :param timeout: optional timeout in seconds for API call response
1432d57dc69SVladimir Kotal    :return return value from given requests method
1442d57dc69SVladimir Kotal    """
1452d57dc69SVladimir Kotal
1462d57dc69SVladimir Kotal    logger = logging.getLogger(__name__)
1472d57dc69SVladimir Kotal
1482d57dc69SVladimir Kotal    if not isinstance(command, dict) or command.get(COMMAND_PROPERTY) is None:
1492d57dc69SVladimir Kotal        raise Exception("invalid command")
1502d57dc69SVladimir Kotal
1512d57dc69SVladimir Kotal    command = command[COMMAND_PROPERTY]
1522d57dc69SVladimir Kotal
1532d57dc69SVladimir Kotal    uri, verb, data, *_ = command
1542d57dc69SVladimir Kotal    try:
1552d57dc69SVladimir Kotal        headers = command[3]
1562d57dc69SVladimir Kotal        if headers and not isinstance(headers, dict):
1572d57dc69SVladimir Kotal            raise Exception("headers must be a dictionary")
1582d57dc69SVladimir Kotal    except IndexError:
1592d57dc69SVladimir Kotal        headers = {}
1602d57dc69SVladimir Kotal
1612d57dc69SVladimir Kotal    if headers is None:
1622d57dc69SVladimir Kotal        headers = {}
1632d57dc69SVladimir Kotal
164b369c884SVladimir Kotal    logger.debug("Headers from the command: {}".format(headers))
16589229afdSVladimir Kotal    if http_headers:
166b369c884SVladimir Kotal        logger.debug("Updating HTTP headers for command {} with {}".
167b369c884SVladimir Kotal                     format(command, http_headers))
16889229afdSVladimir Kotal        headers.update(http_headers)
16989229afdSVladimir Kotal
1702d97c0a2SVladimir Kotal    uri = subst(uri, substitutions)
1712d57dc69SVladimir Kotal    header_names = [x.lower() for x in headers.keys()]
1722d57dc69SVladimir Kotal
1732d57dc69SVladimir Kotal    if data:
1742d57dc69SVladimir Kotal        if CONTENT_TYPE.lower() not in header_names:
1752d57dc69SVladimir Kotal            logger.debug("Adding header: {} = {}".
1762d57dc69SVladimir Kotal                         format(CONTENT_TYPE, APPLICATION_JSON))
1772d57dc69SVladimir Kotal            headers[CONTENT_TYPE] = APPLICATION_JSON
1782d57dc69SVladimir Kotal
1792d57dc69SVladimir Kotal        for (k, v) in headers.items():
1802d57dc69SVladimir Kotal            if k.lower() == CONTENT_TYPE.lower():
1812d57dc69SVladimir Kotal                if headers[k].lower() == APPLICATION_JSON.lower():
1822d57dc69SVladimir Kotal                    logger.debug("Converting {} to JSON".format(data))
1832d57dc69SVladimir Kotal                    data = json.dumps(data)
1842d57dc69SVladimir Kotal                break
1852d57dc69SVladimir Kotal
1862d97c0a2SVladimir Kotal        data = subst(data, substitutions)
1872d57dc69SVladimir Kotal        logger.debug("entity data: {}".format(data))
1882d57dc69SVladimir Kotal
189732be1c2SVladimir Kotal    return do_api_call(verb, uri, headers=headers, data=data, timeout=timeout)
190