xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/restful.py (revision 77181bfb55253648b84f70b2fed51fe68e93836d)
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
30c41895f8SVladimir Kotalfrom .webutil import get_proxies, is_web_uri
312d57dc69SVladimir Kotal
322d57dc69SVladimir KotalCONTENT_TYPE = 'Content-Type'
332d57dc69SVladimir KotalAPPLICATION_JSON = 'application/json'   # default
342d57dc69SVladimir Kotal
352d57dc69SVladimir Kotal
36959b34e9SVladimir Kotaldef call_finished(location_uri, headers, timeout):
37959b34e9SVladimir Kotal    """
38959b34e9SVladimir Kotal    :param location_uri: URI to check the status of API call
39959b34e9SVladimir Kotal    :param headers: HTTP headers
40959b34e9SVladimir Kotal    :param timeout: connect timeout
415cce3163SVladimir Kotal    :return indication and response tuple
42959b34e9SVladimir Kotal    """
43959b34e9SVladimir Kotal    logger = logging.getLogger(__name__)
44959b34e9SVladimir Kotal
45959b34e9SVladimir Kotal    logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}")
46959b34e9SVladimir Kotal    response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
47959b34e9SVladimir Kotal    if response is None:
48959b34e9SVladimir Kotal        raise Exception("API call failed")
49959b34e9SVladimir Kotal
50959b34e9SVladimir Kotal    response.raise_for_status()
51959b34e9SVladimir Kotal    if response.status_code == 202:
525cce3163SVladimir Kotal        return False, response
53959b34e9SVladimir Kotal    else:
545cce3163SVladimir Kotal        return True, response
55959b34e9SVladimir Kotal
56959b34e9SVladimir Kotal
57959b34e9SVladimir Kotaldef wait_for_async_api(response, api_timeout=None, headers=None, timeout=None):
581c258122SVladimir Kotal    """
591c258122SVladimir Kotal    :param response: request
60959b34e9SVladimir 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()
72959b34e9SVladimir Kotal    if api_timeout is None:
73959b34e9SVladimir Kotal        while True:
745cce3163SVladimir Kotal            done, response = call_finished(location_uri, headers, timeout)
755cce3163SVladimir Kotal            if done:
76959b34e9SVladimir Kotal                break
771c258122SVladimir Kotal            time.sleep(1)
781c258122SVladimir Kotal    else:
79959b34e9SVladimir Kotal        for _ in range(api_timeout):
805cce3163SVladimir Kotal            done, response = call_finished(location_uri, headers, timeout)
815cce3163SVladimir Kotal            if done:
821c258122SVladimir Kotal                break
83959b34e9SVladimir Kotal            time.sleep(1)
841c258122SVladimir Kotal
851c258122SVladimir Kotal    if response.status_code == 202:
86cc7022c6SVladimir Kotal        wait_time = time.time() - start_time
87cc7022c6SVladimir Kotal        logger.warn(f"API request still not completed after {int(wait_time)} seconds: {response}")
881c258122SVladimir Kotal        return response
891c258122SVladimir Kotal
901c258122SVladimir Kotal    logger.debug(f"DELETE API call to {location_uri}")
911c258122SVladimir Kotal    requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
921c258122SVladimir Kotal
931c258122SVladimir Kotal    return response
941c258122SVladimir Kotal
951c258122SVladimir Kotal
96c41895f8SVladimir Kotaldef do_api_call(method, uri, params=None, headers=None, data=None, timeout=None, api_timeout=None):
972d57dc69SVladimir Kotal    """
982d57dc69SVladimir Kotal    Perform an API call. Will raise an exception if the request fails.
99c41895f8SVladimir Kotal    :param method: string holding HTTP verb
1002d57dc69SVladimir Kotal    :param uri: URI string
1012d57dc69SVladimir Kotal    :param params: request parameters
1022d57dc69SVladimir Kotal    :param headers: HTTP headers dictionary
1032d57dc69SVladimir Kotal    :param data: data or None
104732be1c2SVladimir Kotal    :param timeout: optional connect timeout in seconds.
1051c258122SVladimir Kotal                    Applies also to asynchronous API status calls.
1061c258122SVladimir Kotal    :param api_timeout: optional timeout for asynchronous API requests in seconds.
1072d57dc69SVladimir Kotal    :return: the result of the handler call, can be None
1082d57dc69SVladimir Kotal    """
1092d57dc69SVladimir Kotal    logger = logging.getLogger(__name__)
1102d57dc69SVladimir Kotal
111c41895f8SVladimir Kotal    handler = getattr(requests, method.lower())
1122d57dc69SVladimir Kotal    if handler is None or not callable(handler):
113c41895f8SVladimir Kotal        raise Exception('Unknown HTTP method: {}'.format(method))
1142d57dc69SVladimir Kotal
115cc7022c6SVladimir Kotal    logger.debug("{} API call: {} with data '{}', connect timeout {} seconds, API timeout {} seconds and headers: {}".
116c41895f8SVladimir Kotal                 format(method, uri, data, timeout, api_timeout, headers))
1172d57dc69SVladimir Kotal    r = handler(
1182d57dc69SVladimir Kotal        uri,
1192d57dc69SVladimir Kotal        data=data,
1202d57dc69SVladimir Kotal        params=params,
1212d57dc69SVladimir Kotal        headers=headers,
122732be1c2SVladimir Kotal        proxies=get_proxies(uri),
123732be1c2SVladimir Kotal        timeout=timeout
1242d57dc69SVladimir Kotal    )
1252d57dc69SVladimir Kotal
1262d57dc69SVladimir Kotal    if r is None:
1272d57dc69SVladimir Kotal        raise Exception("API call failed")
1282d57dc69SVladimir Kotal
1291c258122SVladimir Kotal    if r.status_code == 202:
130959b34e9SVladimir Kotal        r = wait_for_async_api(r, api_timeout=api_timeout, headers=headers, timeout=timeout)
1311c258122SVladimir Kotal
1322d57dc69SVladimir Kotal    r.raise_for_status()
1332d57dc69SVladimir Kotal
1342d57dc69SVladimir Kotal    return r
1352d57dc69SVladimir Kotal
1362d57dc69SVladimir Kotal
1372d97c0a2SVladimir Kotaldef subst(src, substitutions):
1382d97c0a2SVladimir Kotal    if substitutions:
1392d97c0a2SVladimir Kotal        for pattern, value in substitutions.items():
1402d97c0a2SVladimir Kotal            if value:
1412d97c0a2SVladimir Kotal                src = src.replace(pattern, value)
1422d97c0a2SVladimir Kotal
1432d97c0a2SVladimir Kotal    return src
1442d97c0a2SVladimir Kotal
1452d97c0a2SVladimir Kotal
146c41895f8SVladimir Kotaldef get_call_props(call):
1472d57dc69SVladimir Kotal    """
148c41895f8SVladimir Kotal    Retrieve the basic properties of a call.
149c41895f8SVladimir Kotal    :param call: dictionary
150c41895f8SVladimir Kotal    :return: URI, HTTP method, data, headers
151c41895f8SVladimir Kotal    """
152c41895f8SVladimir Kotal
153c41895f8SVladimir Kotal    logger = logging.getLogger(__name__)
154c41895f8SVladimir Kotal
155c41895f8SVladimir Kotal    uri = call.get("uri")
156c41895f8SVladimir Kotal    if not uri:
157c41895f8SVladimir Kotal        raise Exception(f"no 'uri' key present in {call}")
158c41895f8SVladimir Kotal    if not is_web_uri(uri):
159c41895f8SVladimir Kotal        raise Exception(f"not a valid URI: {uri}")
160c41895f8SVladimir Kotal
161c41895f8SVladimir Kotal    method = call.get("method")
162c41895f8SVladimir Kotal    if not method:
163c41895f8SVladimir Kotal        logger.debug(f"no 'method' key in {call}, using GET")
164c41895f8SVladimir Kotal        method = "GET"
165c41895f8SVladimir Kotal
166c41895f8SVladimir Kotal    data = call.get("data")
167c41895f8SVladimir Kotal
168c41895f8SVladimir Kotal    try:
169c41895f8SVladimir Kotal        headers = call.get("headers")
170c41895f8SVladimir Kotal        if headers and not isinstance(headers, dict):
171c41895f8SVladimir Kotal            raise Exception("headers must be a dictionary")
172c41895f8SVladimir Kotal    except IndexError:
173c41895f8SVladimir Kotal        headers = {}
174c41895f8SVladimir Kotal
175c41895f8SVladimir Kotal    if headers is None:
176c41895f8SVladimir Kotal        headers = {}
177c41895f8SVladimir Kotal
178c41895f8SVladimir Kotal    return uri, method, data, headers
179c41895f8SVladimir Kotal
180c41895f8SVladimir Kotal
181c41895f8SVladimir Kotaldef call_rest_api(call, substitutions=None, http_headers=None, timeout=None, api_timeout=None):
182c41895f8SVladimir Kotal    """
183c41895f8SVladimir Kotal    Make REST API call. Occurrence of the pattern in the URI
1842d57dc69SVladimir Kotal    (first part of the command) or data payload will be replaced by the name.
1852d57dc69SVladimir Kotal
1862d57dc69SVladimir Kotal    Default content type is application/json.
1872d57dc69SVladimir Kotal
188c41895f8SVladimir Kotal    :param call: dictionary describing the properties of the API call
1892d97c0a2SVladimir Kotal    :param substitutions: dictionary of pattern:value for command and/or
1902d97c0a2SVladimir Kotal                          data substitution
19189229afdSVladimir Kotal    :param http_headers: optional dictionary of HTTP headers to be appended
192*77181bfbSVladimir Kotal    :param timeout: optional connect/read timeout in seconds for API call
193*77181bfbSVladimir Kotal    :param api_timeout: optional timeout in seconds for total duration of asynchronous API call
194959b34e9SVladimir Kotal    :return value from given requests method
1952d57dc69SVladimir Kotal    """
1962d57dc69SVladimir Kotal
1972d57dc69SVladimir Kotal    logger = logging.getLogger(__name__)
1982d57dc69SVladimir Kotal
199c41895f8SVladimir Kotal    uri, verb, data, headers = get_call_props(call)
2002d57dc69SVladimir Kotal
201c41895f8SVladimir Kotal    logger.debug(f"Headers from the call structure: {headers}")
20289229afdSVladimir Kotal    if http_headers:
203c41895f8SVladimir Kotal        logger.debug("Updating HTTP headers for call {} with {}".
204c41895f8SVladimir Kotal                     format(call, http_headers))
20589229afdSVladimir Kotal        headers.update(http_headers)
20689229afdSVladimir Kotal
207c41895f8SVladimir Kotal    logger.debug("Performing URI substitutions")
2082d97c0a2SVladimir Kotal    uri = subst(uri, substitutions)
209c41895f8SVladimir Kotal    logger.debug(f"URI after the substitutions: {uri}")
210c41895f8SVladimir Kotal
211*77181bfbSVladimir Kotal    call_timeout = call.get("timeout")
212*77181bfbSVladimir Kotal    if call_timeout:
213*77181bfbSVladimir Kotal        logger.debug(f"Setting connect/read API timeout based on the call to {call_timeout}")
214*77181bfbSVladimir Kotal        timeout = call_timeout
215*77181bfbSVladimir Kotal
216c41895f8SVladimir Kotal    call_api_timeout = call.get("api_timeout")
217c41895f8SVladimir Kotal    if call_api_timeout:
218*77181bfbSVladimir Kotal        logger.debug(f"Setting async API timeout based on the call to {call_api_timeout}")
219c41895f8SVladimir Kotal        api_timeout = call_api_timeout
2202d57dc69SVladimir Kotal
2212d57dc69SVladimir Kotal    if data:
222c41895f8SVladimir Kotal        header_names = [x.lower() for x in headers.keys()]
2232d57dc69SVladimir Kotal        if CONTENT_TYPE.lower() not in header_names:
224c41895f8SVladimir Kotal            logger.debug("Adding HTTP header: {} = {}".
2252d57dc69SVladimir Kotal                         format(CONTENT_TYPE, APPLICATION_JSON))
2262d57dc69SVladimir Kotal            headers[CONTENT_TYPE] = APPLICATION_JSON
2272d57dc69SVladimir Kotal
2282d57dc69SVladimir Kotal        for (k, v) in headers.items():
2292d57dc69SVladimir Kotal            if k.lower() == CONTENT_TYPE.lower():
2302d57dc69SVladimir Kotal                if headers[k].lower() == APPLICATION_JSON.lower():
2312d57dc69SVladimir Kotal                    logger.debug("Converting {} to JSON".format(data))
2322d57dc69SVladimir Kotal                    data = json.dumps(data)
2332d57dc69SVladimir Kotal                break
2342d57dc69SVladimir Kotal
2352d97c0a2SVladimir Kotal        data = subst(data, substitutions)
2362d57dc69SVladimir Kotal        logger.debug("entity data: {}".format(data))
2372d57dc69SVladimir Kotal
23816e733aaSVladimir Kotal    return do_api_call(verb, uri, headers=headers, data=data, timeout=timeout, api_timeout=api_timeout)
239