xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/restful.py (revision 7626bae0f6e5cafadf9bedeb56ebdc5d31564565)
1#
2# CDDL HEADER START
3#
4# The contents of this file are subject to the terms of the
5# Common Development and Distribution License (the "License").
6# You may not use this file except in compliance with the License.
7#
8# See LICENSE.txt included in this distribution for the specific
9# language governing permissions and limitations under the License.
10#
11# When distributing Covered Code, include this CDDL HEADER in each
12# file and include the License file at LICENSE.txt.
13# If applicable, add the following below this CDDL HEADER, with the
14# fields enclosed by brackets "[]" replaced with your own identifying
15# information: Portions Copyright [yyyy] [name of copyright owner]
16#
17# CDDL HEADER END
18#
19
20#
21# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved.
22#
23
24import json
25import logging
26import time
27
28import requests
29
30from .webutil import get_proxies, is_web_uri
31
32CONTENT_TYPE = 'Content-Type'
33APPLICATION_JSON = 'application/json'   # default
34
35
36def call_finished(location_uri, headers, timeout):
37    """
38    :param location_uri: URI to check the status of API call
39    :param headers: HTTP headers
40    :param timeout: connect timeout
41    :return indication and response tuple
42    """
43    logger = logging.getLogger(__name__)
44
45    logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}")
46    response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
47    if response is None:
48        raise Exception("API call failed")
49
50    response.raise_for_status()
51    if response.status_code == 202:
52        return False, response
53    else:
54        return True, response
55
56
57def wait_for_async_api(response, api_timeout=None, headers=None, timeout=None):
58    """
59    :param response: request
60    :param api_timeout: asynchronous API timeout (will wait forever or until error if None)
61    :param headers: request headers
62    :param timeout: connect timeout
63    :return: request
64    """
65    logger = logging.getLogger(__name__)
66
67    location_uri = response.headers.get("Location")
68    if location_uri is None:
69        raise Exception(f"no Location header in {response}")
70
71    start_time = time.time()
72    if api_timeout is None:
73        while True:
74            done, response = call_finished(location_uri, headers, timeout)
75            if done:
76                break
77            time.sleep(1)
78    else:
79        for _ in range(api_timeout):
80            done, response = call_finished(location_uri, headers, timeout)
81            if done:
82                break
83            time.sleep(1)
84
85    if response.status_code == 202:
86        wait_time = time.time() - start_time
87        logger.warn(f"API request still not completed after {int(wait_time)} seconds: {response}")
88        return response
89
90    logger.debug(f"DELETE API call to {location_uri}")
91    requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout)
92
93    return response
94
95
96def do_api_call(method, uri, params=None, headers=None, data=None, timeout=None, api_timeout=None):
97    """
98    Perform an API call. Will raise an exception if the request fails.
99    :param method: string holding HTTP verb
100    :param uri: URI string
101    :param params: request parameters
102    :param headers: HTTP headers dictionary
103    :param data: data or None
104    :param timeout: optional connect timeout in seconds.
105                    Applies also to asynchronous API status calls.
106    :param api_timeout: optional timeout for asynchronous API requests in seconds.
107    :return: the result of the handler call, can be None
108    """
109    logger = logging.getLogger(__name__)
110
111    handler = getattr(requests, method.lower())
112    if handler is None or not callable(handler):
113        raise Exception('Unknown HTTP method: {}'.format(method))
114
115    logger.debug("{} API call: {} with data '{}', connect timeout {} seconds, API timeout {} seconds and headers: {}".
116                 format(method, uri, data, timeout, api_timeout, headers))
117    r = handler(
118        uri,
119        data=data,
120        params=params,
121        headers=headers,
122        proxies=get_proxies(uri),
123        timeout=timeout
124    )
125    logger.debug(f"API call result: {r}")
126
127    if r is None:
128        raise Exception("API call failed")
129
130    if r.status_code == 202:
131        r = wait_for_async_api(r, api_timeout=api_timeout, headers=headers, timeout=timeout)
132
133    r.raise_for_status()
134
135    return r
136
137
138def subst(src, substitutions):
139    if substitutions:
140        for pattern, value in substitutions.items():
141            if value:
142                src = src.replace(pattern, value)
143
144    return src
145
146
147def call_rest_api(call, substitutions=None, http_headers=None, timeout=None, api_timeout=None):
148    """
149    Make REST API call. Occurrence of the pattern in the URI
150    (first part of the command) or data payload will be replaced by the name.
151
152    Default content type is application/json.
153
154    :param call: ApiCall object
155    :param substitutions: dictionary of pattern:value for command and/or
156                          data substitution
157    :param http_headers: optional dictionary of HTTP headers to be appended
158    :param timeout: optional connect/read timeout in seconds for API call
159    :param api_timeout: optional timeout in seconds for total duration of asynchronous API call
160    :return value from given requests method
161    """
162
163    logger = logging.getLogger(__name__)
164
165    logger.debug(f"Headers from the ApiCall object: {call.headers}")
166    headers = call.headers
167    if http_headers:
168        logger.debug("Updating HTTP headers for API call {} with {}".
169                     format(call, http_headers))
170        headers.update(http_headers)
171
172    logger.debug("Performing URI substitutions")
173    uri = subst(call.uri, substitutions)
174    logger.debug(f"URI after the substitutions: {uri}")
175
176    if not is_web_uri(uri):
177        raise Exception(f"not a valid URI: {uri}")
178
179    call_timeout = call.api_timeout
180    if call_timeout:
181        logger.debug(f"Setting connect/read API timeout based on the call to {call_timeout}")
182        timeout = call_timeout
183
184    call_api_timeout = call.async_api_timeout
185    if call_api_timeout:
186        logger.debug(f"Setting async API timeout based on the call to {call_api_timeout}")
187        api_timeout = call_api_timeout
188
189    data = call.data
190    if data:
191        header_names = [x.lower() for x in headers.keys()]
192        if CONTENT_TYPE.lower() not in header_names:
193            logger.debug("Adding HTTP header: {} = {}".
194                         format(CONTENT_TYPE, APPLICATION_JSON))
195            headers[CONTENT_TYPE] = APPLICATION_JSON
196
197        for (k, v) in headers.items():
198            if k.lower() == CONTENT_TYPE.lower():
199                if headers[k].lower() == APPLICATION_JSON.lower():
200                    logger.debug("Converting {} to JSON".format(data))
201                    data = json.dumps(data)
202                break
203
204        data = subst(data, substitutions)
205        logger.debug("entity data: {}".format(data))
206
207    return do_api_call(call.method, uri, headers=headers, data=data, timeout=timeout, api_timeout=api_timeout)
208