Cara menggunakan python requests exceptions documentation

Pengembangan web adalah istilah umum untuk membuat konsep, membuat, menyebarkan, dan mengoperasikan aplikasi website dan antarmuka pemrograman aplikasi untuk Website.

Penggunaan Python dalam Pengembangan Web

Python dapat digunakan untuk membangun aplikasi web sisi server. Sementara kerangka web tidak diperlukan untuk membangun aplikasi web, jarang sekali pengembang tidak akan menggunakan pustaka sumber terbuka yang ada untuk mempercepat kemajuan mereka dalam membuat aplikasi mereka berfungsi.

Python tidak digunakan di browser web. Bahasa yang dijalankan di browser seperti Chrome, Firefox, dan Internet Explorer adalah JavaScript. Proyek seperti pyjs dapat dikompilasi dari Python ke JavaScript. Namun, sebagian besar pengembang Python menulis aplikasi web mereka menggunakan kombinasi Python dan JavaScript. Python dieksekusi di sisi server sementara JavaScript diunduh ke klien dan dijalankan oleh browser web.

Untuk membuat website dengan menggunakan Python sebagai bahasa pemrogramanya, caranya sangat mudah. Tetapi perlu diingat bahwa sebelumnya Anda sudah harus menguasai HTML, CSS dan Javascript.

Web Framework Python

Framework pengembangan web pada python yang paling populer dan mudah dipelajari ada Django dan Flask

Flask

Flask adalah sebuah microframework web python yang mudah untuk dipelajari, mudah diinstal dan pengembangan yang sangat simpel.

Berikut adalah beberapa kelebihanya :

  • mudah digunakan.
  • dibangun di server pengembangan dan debugger
  • dukungan pengujian unit terpadu
  • Kirim permintaan yang tenang
  • menggunakan tempering Jinja2
  • dukungan untuk cookie aman (sesi sisi klien)
  • 100% WSGI 1.0 compliant
  • Berbasis Unicode
  • didokumentasikan secara ekstensif

Instalasi Flask pip install Flask

Hello World Web App dengan Flask

from flask import Flask
app = Flask(**name**)

@app.route("/")
def hello():
return "Hello World!"
<<<<<<<< HEAD:_tutorial/pengembangan-web-python.md

if **name** == "**main**":
app.run()
{% endhighlight %}

========

if **name** == "**main**":
app.run()

pr/80:docs/tutorial/pengembangan-web-python.md Jalankan server dengan perintah: python hello.py

Buka http://localhost:5000/ dibrowser anda dan akan muncul Hello World!

Django

Django adalah kerangka kerja Python Web tingkat tinggi yang menangani banyak kerumitan pengembangan Web, sehingga Anda dapat fokus untuk menulis aplikasi tanpa perlu menemukan kembali roda.

Kelebihan Framework Django dibanding yang lain adalah pada segi skalabilitas. Framework ini cocok untuk pengembangan aplikasi besar.

Untuk menginstal Django jalankan perintah dibawah ini : pip install Django==1.7.1

Setelah terinstal, buat direktori /django-hello/ untuk aplikasi Anda. Dalam direktori ini buat file hello.py dengan code dibawah ini:

Here's a generic way to do things which at least means that you don't have to surround each and every requests call with try ... except:

# see the docs: if you set no timeout the call never times out! A tuple means "max 
# connect time" and "max read time"
DEFAULT_REQUESTS_TIMEOUT = (5, 15) # for example

def log_exception(e, verb, url, kwargs):
    # the reason for making this a separate function will become apparent
    raw_tb = traceback.extract_stack()
    if 'data' in kwargs and len(kwargs['data']) > 500: # anticipate giant data string
        kwargs['data'] = f'{kwargs["data"][:500]}...'  
    msg = f'BaseException raised: {e.__class__.__module__}.{e.__class__.__qualname__}: {e}\n' \
        + f'verb {verb}, url {url}, kwargs {kwargs}\n\n' \
        + 'Stack trace:\n' + ''.join(traceback.format_list(raw_tb[:-2]))
    logger.error(msg) 

def requests_call(verb, url, **kwargs):
    response = None
    exception = None
    try:
        if 'timeout' not in kwargs:
            kwargs['timeout'] = DEFAULT_REQUESTS_TIMEOUT
        response = requests.request(verb, url, **kwargs)
    except BaseException as e:
        log_exception(e, verb, url, kwargs)
        exception = e
    return (response, exception)

NB

  1. Be aware of ConnectionError which is a , nothing to do with the class requests.ConnectionError*. I assume the latter is more common in this context but have no real idea...
  2. When examining a non-None returned exception,
    search_response, exception = utilities.requests_call('get',
        f'http://localhost:9200/my_index/_search?q={search_string}')
    
    0, the superclass of all the requests exceptions (including requests.ConnectionError), is not "
    search_response, exception = utilities.requests_call('get',
        f'http://localhost:9200/my_index/_search?q={search_string}')
    
    3" according to . Maybe it has changed since the accepted answer.**
  3. Obviously this assumes a logger has been configured. Calling
    search_response, exception = utilities.requests_call('get',
        f'http://localhost:9200/my_index/_search?q={search_string}')
    
    4 in the
    search_response, exception = utilities.requests_call('get',
        f'http://localhost:9200/my_index/_search?q={search_string}')
    
    5 block might seem a good idea but that would only give the stack within this method! Instead, get the trace leading up to the call to this method. Then log (with details of the exception, and of the call which caused the problem)

*I looked at the source code: requests.ConnectionError subclasses the single class

search_response, exception = utilities.requests_call('get',
    f'http://localhost:9200/my_index/_search?q={search_string}')
0, which subclasses the single class
search_response, exception = utilities.requests_call('get',
    f'http://localhost:9200/my_index/_search?q={search_string}')
8 (builtin)

**However at the bottom of you find "requests.exceptions.RequestException" at the time of writing (2022-02)... but it links to the above page: confusing.


Usage is very simple:

search_response, exception = utilities.requests_call('get',
    f'http://localhost:9200/my_index/_search?q={search_string}')

First you check the response: if it's None something funny has happened and you will have an exception which has to be acted on in some way depending on context (and on the exception). In Gui applications (PyQt5) I usually implement a "visual log" to give some output to the user (and also log simultaneously to the log file), but messages added there should be non-technical. So something like this might typically follow:

if search_response == None:
    # you might check here for (e.g.) a requests.Timeout, tailoring the message
    # accordingly, as the kind of error anyone might be expected to understand
    msg = f'No response searching on |{search_string}|. See log'
    MainWindow.the().visual_log(msg, log_level=logging.ERROR)
    return
response_json = search_response.json()
if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
    msg = f'Bad response searching on |{search_string}|. See log'
    MainWindow.the().visual_log(msg, log_level=logging.ERROR)
    # usually response_json will give full details about the problem
    log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
    logger.error(log_msg)
    return

# now examine the keys and values in response_json: these may of course 
# indicate an error of some kind even though the response returned OK (status 200)... 

Given that the stack trace is logged automatically you often need no more than that...

(... potentially sparing a great deal of boilerplate!)

To cross the Ts, when a json object is expected to be returned:

If, as above, an exception gives your non-technical user a message "No response", and a non-200 status "Bad response", I suggest that

  • a missing expected key in the response's JSON structure should give rise to a message "Anomalous response"
  • an out-of-range or strange value to a message "Unexpected response"
  • and the presence of a key such as "error" or "errors", with value
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    0 or whatever, to a message "Error response"

These may or may not prevent the code from continuing.


... and in fact to my mind it is worth making the process even more generic. These next functions, for me, typically cut down 20 lines of code using the above

if search_response == None:
    # you might check here for (e.g.) a requests.Timeout, tailoring the message
    # accordingly, as the kind of error anyone might be expected to understand
    msg = f'No response searching on |{search_string}|. See log'
    MainWindow.the().visual_log(msg, log_level=logging.ERROR)
    return
response_json = search_response.json()
if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
    msg = f'Bad response searching on |{search_string}|. See log'
    MainWindow.the().visual_log(msg, log_level=logging.ERROR)
    # usually response_json will give full details about the problem
    log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
    logger.error(log_msg)
    return

# now examine the keys and values in response_json: these may of course 
# indicate an error of some kind even though the response returned OK (status 200)... 
1 to about 3, and make most of your handling and your log messages standardised. More than a handful of requests calls in your project and the code gets a lot nicer and less bloated:

def log_response_error(response_type, call_name, deliverable, verb, url, **kwargs):
    # NB this function can also be used independently
    if response_type == 'No': # exception was raised (and logged)
        if isinstance(deliverable, requests.Timeout):
            MainWindow.the().visual_log(f'Time out of {call_name} before response received!', logging.ERROR)
            return    
    else:
        if isinstance(deliverable, BaseException):
            # NB if response.json() raises an exception we end up here
            log_exception(deliverable, verb, url, kwargs)
        else:
            # if we get here no exception has been raised, so no stack trace has yet been logged.  
            # a response has been returned, but is either "Bad" or "Anomalous"
            response_json = deliverable.json()

            raw_tb = traceback.extract_stack()
            if 'data' in kwargs and len(kwargs['data']) > 500: # anticipate giant data string
                kwargs['data'] = f'{kwargs["data"][:500]}...'
            added_message = ''     
            if hasattr(deliverable, 'added_message'):
                added_message = deliverable.added_message + '\n'
                del deliverable.added_message
            call_and_response_details = f'{response_type} response\n{added_message}' \
                + f'verb {verb}, url {url}, kwargs {kwargs}\nresponse:\n{json.dumps(response_json, indent=4)}'
            logger.error(f'{call_and_response_details}\nStack trace: {"".join(traceback.format_list(raw_tb[:-1]))}')
    MainWindow.the().visual_log(f'{response_type} response {call_name}. See log.', logging.ERROR)
    
def check_keys(req_dict_structure, response_dict_structure, response):
    # so this function is about checking the keys in the returned json object...
    # NB both structures MUST be dicts
    if not isinstance(req_dict_structure, dict):
        response.added_message = f'req_dict_structure not dict: {type(req_dict_structure)}\n'
        return False
    if not isinstance(response_dict_structure, dict):
        response.added_message = f'response_dict_structure not dict: {type(response_dict_structure)}\n'
        return False
    for dict_key in req_dict_structure.keys():
        if dict_key not in response_dict_structure:
            response.added_message = f'key |{dict_key}| missing\n'
            return False
        req_value = req_dict_structure[dict_key]
        response_value = response_dict_structure[dict_key]
        if isinstance(req_value, dict):
            # if the response at this point is a list apply the req_value dict to each element:
            # failure in just one such element leads to "Anomalous response"... 
            if isinstance(response_value, list):
                for resp_list_element in response_value:
                    if not check_keys(req_value, resp_list_element, response):
                        return False
            elif not check_keys(req_value, response_value, response): # any other response value must be a dict (tested in next level of recursion)
                return False
        elif isinstance(req_value, list):
            if not isinstance(response_value, list): # if the req_value is a list the reponse must be one
                response.added_message = f'key |{dict_key}| not list: {type(response_value)}\n'
                return False
            # it is OK for the value to be a list, but these must be strings (keys) or dicts
            for req_list_element, resp_list_element in zip(req_value, response_value):
                if isinstance(req_list_element, dict):
                    if not check_keys(req_list_element, resp_list_element, response):
                        return False
                if not isinstance(req_list_element, str):
                    response.added_message = f'req_list_element not string: {type(req_list_element)}\n'
                    return False
                if req_list_element not in response_value:
                    response.added_message = f'key |{req_list_element}| missing from response list\n'
                    return False
        # put None as a dummy value (otherwise something like {'my_key'} will be seen as a set, not a dict 
        elif req_value != None: 
            response.added_message = f'required value of key |{dict_key}| must be None (dummy), dict or list: {type(req_value)}\n'
            return False
    return True

def process_json_requests_call(verb, url, **kwargs):
    # "call_name" is a mandatory kwarg
    if 'call_name' not in kwargs:
        raise Exception('kwarg "call_name" not supplied!')
    call_name = kwargs['call_name']
    del kwargs['call_name']

    required_keys = {}    
    if 'required_keys' in kwargs:
        required_keys = kwargs['required_keys']
        del kwargs['required_keys']

    acceptable_statuses = [200]
    if 'acceptable_statuses' in kwargs:
        acceptable_statuses = kwargs['acceptable_statuses']
        del kwargs['acceptable_statuses']

    exception_handler = log_response_error
    if 'exception_handler' in kwargs:
        exception_handler = kwargs['exception_handler']
        del kwargs['exception_handler']
        
    response, exception = requests_call(verb, url, **kwargs)

    if response == None:
        exception_handler('No', call_name, exception, verb, url, **kwargs)
        return (False, exception)
    try:
        response_json = response.json()
    except BaseException as e:
        logger.error(f'response.status_code {response.status_code} but calling json() raised exception')
        # an exception raised at this point can't truthfully lead to a "No response" message... so say "bad"
        exception_handler('Bad', call_name, e, verb, url, **kwargs)
        return (False, response)
        
    status_ok = response.status_code in acceptable_statuses
    if not status_ok:
        response.added_message = f'status code was {response.status_code}'
        log_response_error('Bad', call_name, response, verb, url, **kwargs)
        return (False, response)
    check_result = check_keys(required_keys, response_json, response)
    if not check_result:
        log_response_error('Anomalous', call_name, response, verb, url, **kwargs)
    return (check_result, response)      

Example call (NB with this version, the "deliverable" is either an exception or a response which delivers a json structure):

success, deliverable = utilities.process_json_requests_call('get', 
    f'{ES_URL}{INDEX_NAME}/_doc/1', 
    call_name=f'checking index {INDEX_NAME}',
    required_keys={'_source':{'status_text': None}})
if not success: return False
# here, we know the deliverable is a response, not an exception
# we also don't need to check for the keys being present: 
# the generic code has checked that all expected keys are present
index_status = deliverable.json()['_source']['status_text']
if index_status != 'successfully completed':
    # ... i.e. an example of a 200 response, but an error nonetheless
    msg = f'Error response: ES index {INDEX_NAME} does not seem to have been built OK: cannot search'
    MainWindow.the().visual_log(msg)
    logger.error(f'index |{INDEX_NAME}|: deliverable.json() {json.dumps(deliverable.json(), indent=4)}')
    return False

So the "visual log" message seen by the user in the case of missing key "status_text", for example, would be "Anomalous response checking index XYZ. See log." (and the log would give a more detailed technical message, constructed automatically, including the stack trace but also details of the missing key in question).

NB

  • mandatory kwarg:
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    3; optional kwargs:
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    4,
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    5,
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    6.
  • the
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    4
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    8 can be nested to any depth
  • finer-grained exception-handling can be accomplished by including a function
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    6 in
    def log_response_error(response_type, call_name, deliverable, verb, url, **kwargs):
        # NB this function can also be used independently
        if response_type == 'No': # exception was raised (and logged)
            if isinstance(deliverable, requests.Timeout):
                MainWindow.the().visual_log(f'Time out of {call_name} before response received!', logging.ERROR)
                return    
        else:
            if isinstance(deliverable, BaseException):
                # NB if response.json() raises an exception we end up here
                log_exception(deliverable, verb, url, kwargs)
            else:
                # if we get here no exception has been raised, so no stack trace has yet been logged.  
                # a response has been returned, but is either "Bad" or "Anomalous"
                response_json = deliverable.json()
    
                raw_tb = traceback.extract_stack()
                if 'data' in kwargs and len(kwargs['data']) > 500: # anticipate giant data string
                    kwargs['data'] = f'{kwargs["data"][:500]}...'
                added_message = ''     
                if hasattr(deliverable, 'added_message'):
                    added_message = deliverable.added_message + '\n'
                    del deliverable.added_message
                call_and_response_details = f'{response_type} response\n{added_message}' \
                    + f'verb {verb}, url {url}, kwargs {kwargs}\nresponse:\n{json.dumps(response_json, indent=4)}'
                logger.error(f'{call_and_response_details}\nStack trace: {"".join(traceback.format_list(raw_tb[:-1]))}')
        MainWindow.the().visual_log(f'{response_type} response {call_name}. See log.', logging.ERROR)
        
    def check_keys(req_dict_structure, response_dict_structure, response):
        # so this function is about checking the keys in the returned json object...
        # NB both structures MUST be dicts
        if not isinstance(req_dict_structure, dict):
            response.added_message = f'req_dict_structure not dict: {type(req_dict_structure)}\n'
            return False
        if not isinstance(response_dict_structure, dict):
            response.added_message = f'response_dict_structure not dict: {type(response_dict_structure)}\n'
            return False
        for dict_key in req_dict_structure.keys():
            if dict_key not in response_dict_structure:
                response.added_message = f'key |{dict_key}| missing\n'
                return False
            req_value = req_dict_structure[dict_key]
            response_value = response_dict_structure[dict_key]
            if isinstance(req_value, dict):
                # if the response at this point is a list apply the req_value dict to each element:
                # failure in just one such element leads to "Anomalous response"... 
                if isinstance(response_value, list):
                    for resp_list_element in response_value:
                        if not check_keys(req_value, resp_list_element, response):
                            return False
                elif not check_keys(req_value, response_value, response): # any other response value must be a dict (tested in next level of recursion)
                    return False
            elif isinstance(req_value, list):
                if not isinstance(response_value, list): # if the req_value is a list the reponse must be one
                    response.added_message = f'key |{dict_key}| not list: {type(response_value)}\n'
                    return False
                # it is OK for the value to be a list, but these must be strings (keys) or dicts
                for req_list_element, resp_list_element in zip(req_value, response_value):
                    if isinstance(req_list_element, dict):
                        if not check_keys(req_list_element, resp_list_element, response):
                            return False
                    if not isinstance(req_list_element, str):
                        response.added_message = f'req_list_element not string: {type(req_list_element)}\n'
                        return False
                    if req_list_element not in response_value:
                        response.added_message = f'key |{req_list_element}| missing from response list\n'
                        return False
            # put None as a dummy value (otherwise something like {'my_key'} will be seen as a set, not a dict 
            elif req_value != None: 
                response.added_message = f'required value of key |{dict_key}| must be None (dummy), dict or list: {type(req_value)}\n'
                return False
        return True
    
    def process_json_requests_call(verb, url, **kwargs):
        # "call_name" is a mandatory kwarg
        if 'call_name' not in kwargs:
            raise Exception('kwarg "call_name" not supplied!')
        call_name = kwargs['call_name']
        del kwargs['call_name']
    
        required_keys = {}    
        if 'required_keys' in kwargs:
            required_keys = kwargs['required_keys']
            del kwargs['required_keys']
    
        acceptable_statuses = [200]
        if 'acceptable_statuses' in kwargs:
            acceptable_statuses = kwargs['acceptable_statuses']
            del kwargs['acceptable_statuses']
    
        exception_handler = log_response_error
        if 'exception_handler' in kwargs:
            exception_handler = kwargs['exception_handler']
            del kwargs['exception_handler']
            
        response, exception = requests_call(verb, url, **kwargs)
    
        if response == None:
            exception_handler('No', call_name, exception, verb, url, **kwargs)
            return (False, exception)
        try:
            response_json = response.json()
        except BaseException as e:
            logger.error(f'response.status_code {response.status_code} but calling json() raised exception')
            # an exception raised at this point can't truthfully lead to a "No response" message... so say "bad"
            exception_handler('Bad', call_name, e, verb, url, **kwargs)
            return (False, response)
            
        status_ok = response.status_code in acceptable_statuses
        if not status_ok:
            response.added_message = f'status code was {response.status_code}'
            log_response_error('Bad', call_name, response, verb, url, **kwargs)
            return (False, response)
        check_result = check_keys(required_keys, response_json, response)
        if not check_result:
            log_response_error('Anomalous', call_name, response, verb, url, **kwargs)
        return (check_result, response)      
    
    0 (though don't forget that
    if search_response == None:
        # you might check here for (e.g.) a requests.Timeout, tailoring the message
        # accordingly, as the kind of error anyone might be expected to understand
        msg = f'No response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        return
    response_json = search_response.json()
    if search_response.status_code != 200: # NB 201 ("created") may be acceptable sometimes... 
        msg = f'Bad response searching on |{search_string}|. See log'
        MainWindow.the().visual_log(msg, log_level=logging.ERROR)
        # usually response_json will give full details about the problem
        log_msg = f'search on |{search_string}| bad response\n{json.dumps(response_json, indent=4)}'
        logger.error(log_msg)
        return
    
    # now examine the keys and values in response_json: these may of course 
    # indicate an error of some kind even though the response returned OK (status 200)... 
    
    1 will have logged the call details, the exception type and
    def log_response_error(response_type, call_name, deliverable, verb, url, **kwargs):
        # NB this function can also be used independently
        if response_type == 'No': # exception was raised (and logged)
            if isinstance(deliverable, requests.Timeout):
                MainWindow.the().visual_log(f'Time out of {call_name} before response received!', logging.ERROR)
                return    
        else:
            if isinstance(deliverable, BaseException):
                # NB if response.json() raises an exception we end up here
                log_exception(deliverable, verb, url, kwargs)
            else:
                # if we get here no exception has been raised, so no stack trace has yet been logged.  
                # a response has been returned, but is either "Bad" or "Anomalous"
                response_json = deliverable.json()
    
                raw_tb = traceback.extract_stack()
                if 'data' in kwargs and len(kwargs['data']) > 500: # anticipate giant data string
                    kwargs['data'] = f'{kwargs["data"][:500]}...'
                added_message = ''     
                if hasattr(deliverable, 'added_message'):
                    added_message = deliverable.added_message + '\n'
                    del deliverable.added_message
                call_and_response_details = f'{response_type} response\n{added_message}' \
                    + f'verb {verb}, url {url}, kwargs {kwargs}\nresponse:\n{json.dumps(response_json, indent=4)}'
                logger.error(f'{call_and_response_details}\nStack trace: {"".join(traceback.format_list(raw_tb[:-1]))}')
        MainWindow.the().visual_log(f'{response_type} response {call_name}. See log.', logging.ERROR)
        
    def check_keys(req_dict_structure, response_dict_structure, response):
        # so this function is about checking the keys in the returned json object...
        # NB both structures MUST be dicts
        if not isinstance(req_dict_structure, dict):
            response.added_message = f'req_dict_structure not dict: {type(req_dict_structure)}\n'
            return False
        if not isinstance(response_dict_structure, dict):
            response.added_message = f'response_dict_structure not dict: {type(response_dict_structure)}\n'
            return False
        for dict_key in req_dict_structure.keys():
            if dict_key not in response_dict_structure:
                response.added_message = f'key |{dict_key}| missing\n'
                return False
            req_value = req_dict_structure[dict_key]
            response_value = response_dict_structure[dict_key]
            if isinstance(req_value, dict):
                # if the response at this point is a list apply the req_value dict to each element:
                # failure in just one such element leads to "Anomalous response"... 
                if isinstance(response_value, list):
                    for resp_list_element in response_value:
                        if not check_keys(req_value, resp_list_element, response):
                            return False
                elif not check_keys(req_value, response_value, response): # any other response value must be a dict (tested in next level of recursion)
                    return False
            elif isinstance(req_value, list):
                if not isinstance(response_value, list): # if the req_value is a list the reponse must be one
                    response.added_message = f'key |{dict_key}| not list: {type(response_value)}\n'
                    return False
                # it is OK for the value to be a list, but these must be strings (keys) or dicts
                for req_list_element, resp_list_element in zip(req_value, response_value):
                    if isinstance(req_list_element, dict):
                        if not check_keys(req_list_element, resp_list_element, response):
                            return False
                    if not isinstance(req_list_element, str):
                        response.added_message = f'req_list_element not string: {type(req_list_element)}\n'
                        return False
                    if req_list_element not in response_value:
                        response.added_message = f'key |{req_list_element}| missing from response list\n'
                        return False
            # put None as a dummy value (otherwise something like {'my_key'} will be seen as a set, not a dict 
            elif req_value != None: 
                response.added_message = f'required value of key |{dict_key}| must be None (dummy), dict or list: {type(req_value)}\n'
                return False
        return True
    
    def process_json_requests_call(verb, url, **kwargs):
        # "call_name" is a mandatory kwarg
        if 'call_name' not in kwargs:
            raise Exception('kwarg "call_name" not supplied!')
        call_name = kwargs['call_name']
        del kwargs['call_name']
    
        required_keys = {}    
        if 'required_keys' in kwargs:
            required_keys = kwargs['required_keys']
            del kwargs['required_keys']
    
        acceptable_statuses = [200]
        if 'acceptable_statuses' in kwargs:
            acceptable_statuses = kwargs['acceptable_statuses']
            del kwargs['acceptable_statuses']
    
        exception_handler = log_response_error
        if 'exception_handler' in kwargs:
            exception_handler = kwargs['exception_handler']
            del kwargs['exception_handler']
            
        response, exception = requests_call(verb, url, **kwargs)
    
        if response == None:
            exception_handler('No', call_name, exception, verb, url, **kwargs)
            return (False, exception)
        try:
            response_json = response.json()
        except BaseException as e:
            logger.error(f'response.status_code {response.status_code} but calling json() raised exception')
            # an exception raised at this point can't truthfully lead to a "No response" message... so say "bad"
            exception_handler('Bad', call_name, e, verb, url, **kwargs)
            return (False, response)
            
        status_ok = response.status_code in acceptable_statuses
        if not status_ok:
            response.added_message = f'status code was {response.status_code}'
            log_response_error('Bad', call_name, response, verb, url, **kwargs)
            return (False, response)
        check_result = check_keys(required_keys, response_json, response)
        if not check_result:
            log_response_error('Anomalous', call_name, response, verb, url, **kwargs)
        return (check_result, response)      
    
    2, and the stack trace).
  • in the above I also implement a check on key "data" in any
    def log_response_error(response_type, call_name, deliverable, verb, url, **kwargs):
        # NB this function can also be used independently
        if response_type == 'No': # exception was raised (and logged)
            if isinstance(deliverable, requests.Timeout):
                MainWindow.the().visual_log(f'Time out of {call_name} before response received!', logging.ERROR)
                return    
        else:
            if isinstance(deliverable, BaseException):
                # NB if response.json() raises an exception we end up here
                log_exception(deliverable, verb, url, kwargs)
            else:
                # if we get here no exception has been raised, so no stack trace has yet been logged.  
                # a response has been returned, but is either "Bad" or "Anomalous"
                response_json = deliverable.json()
    
                raw_tb = traceback.extract_stack()
                if 'data' in kwargs and len(kwargs['data']) > 500: # anticipate giant data string
                    kwargs['data'] = f'{kwargs["data"][:500]}...'
                added_message = ''     
                if hasattr(deliverable, 'added_message'):
                    added_message = deliverable.added_message + '\n'
                    del deliverable.added_message
                call_and_response_details = f'{response_type} response\n{added_message}' \
                    + f'verb {verb}, url {url}, kwargs {kwargs}\nresponse:\n{json.dumps(response_json, indent=4)}'
                logger.error(f'{call_and_response_details}\nStack trace: {"".join(traceback.format_list(raw_tb[:-1]))}')
        MainWindow.the().visual_log(f'{response_type} response {call_name}. See log.', logging.ERROR)
        
    def check_keys(req_dict_structure, response_dict_structure, response):
        # so this function is about checking the keys in the returned json object...
        # NB both structures MUST be dicts
        if not isinstance(req_dict_structure, dict):
            response.added_message = f'req_dict_structure not dict: {type(req_dict_structure)}\n'
            return False
        if not isinstance(response_dict_structure, dict):
            response.added_message = f'response_dict_structure not dict: {type(response_dict_structure)}\n'
            return False
        for dict_key in req_dict_structure.keys():
            if dict_key not in response_dict_structure:
                response.added_message = f'key |{dict_key}| missing\n'
                return False
            req_value = req_dict_structure[dict_key]
            response_value = response_dict_structure[dict_key]
            if isinstance(req_value, dict):
                # if the response at this point is a list apply the req_value dict to each element:
                # failure in just one such element leads to "Anomalous response"... 
                if isinstance(response_value, list):
                    for resp_list_element in response_value:
                        if not check_keys(req_value, resp_list_element, response):
                            return False
                elif not check_keys(req_value, response_value, response): # any other response value must be a dict (tested in next level of recursion)
                    return False
            elif isinstance(req_value, list):
                if not isinstance(response_value, list): # if the req_value is a list the reponse must be one
                    response.added_message = f'key |{dict_key}| not list: {type(response_value)}\n'
                    return False
                # it is OK for the value to be a list, but these must be strings (keys) or dicts
                for req_list_element, resp_list_element in zip(req_value, response_value):
                    if isinstance(req_list_element, dict):
                        if not check_keys(req_list_element, resp_list_element, response):
                            return False
                    if not isinstance(req_list_element, str):
                        response.added_message = f'req_list_element not string: {type(req_list_element)}\n'
                        return False
                    if req_list_element not in response_value:
                        response.added_message = f'key |{req_list_element}| missing from response list\n'
                        return False
            # put None as a dummy value (otherwise something like {'my_key'} will be seen as a set, not a dict 
            elif req_value != None: 
                response.added_message = f'required value of key |{dict_key}| must be None (dummy), dict or list: {type(req_value)}\n'
                return False
        return True
    
    def process_json_requests_call(verb, url, **kwargs):
        # "call_name" is a mandatory kwarg
        if 'call_name' not in kwargs:
            raise Exception('kwarg "call_name" not supplied!')
        call_name = kwargs['call_name']
        del kwargs['call_name']
    
        required_keys = {}    
        if 'required_keys' in kwargs:
            required_keys = kwargs['required_keys']
            del kwargs['required_keys']
    
        acceptable_statuses = [200]
        if 'acceptable_statuses' in kwargs:
            acceptable_statuses = kwargs['acceptable_statuses']
            del kwargs['acceptable_statuses']
    
        exception_handler = log_response_error
        if 'exception_handler' in kwargs:
            exception_handler = kwargs['exception_handler']
            del kwargs['exception_handler']
            
        response, exception = requests_call(verb, url, **kwargs)
    
        if response == None:
            exception_handler('No', call_name, exception, verb, url, **kwargs)
            return (False, exception)
        try:
            response_json = response.json()
        except BaseException as e:
            logger.error(f'response.status_code {response.status_code} but calling json() raised exception')
            # an exception raised at this point can't truthfully lead to a "No response" message... so say "bad"
            exception_handler('Bad', call_name, e, verb, url, **kwargs)
            return (False, response)
            
        status_ok = response.status_code in acceptable_statuses
        if not status_ok:
            response.added_message = f'status code was {response.status_code}'
            log_response_error('Bad', call_name, response, verb, url, **kwargs)
            return (False, response)
        check_result = check_keys(required_keys, response_json, response)
        if not check_result:
            log_response_error('Anomalous', call_name, response, verb, url, **kwargs)
        return (check_result, response)      
    
    0 which may be logged. This is because a bulk operation (e.g. to populate an index in the case of Elasticsearch) can consist of enormous strings. So curtail to the first 500 characters, for example.

PS Yes, I do know about the

def log_response_error(response_type, call_name, deliverable, verb, url, **kwargs):
    # NB this function can also be used independently
    if response_type == 'No': # exception was raised (and logged)
        if isinstance(deliverable, requests.Timeout):
            MainWindow.the().visual_log(f'Time out of {call_name} before response received!', logging.ERROR)
            return    
    else:
        if isinstance(deliverable, BaseException):
            # NB if response.json() raises an exception we end up here
            log_exception(deliverable, verb, url, kwargs)
        else:
            # if we get here no exception has been raised, so no stack trace has yet been logged.  
            # a response has been returned, but is either "Bad" or "Anomalous"
            response_json = deliverable.json()

            raw_tb = traceback.extract_stack()
            if 'data' in kwargs and len(kwargs['data']) > 500: # anticipate giant data string
                kwargs['data'] = f'{kwargs["data"][:500]}...'
            added_message = ''     
            if hasattr(deliverable, 'added_message'):
                added_message = deliverable.added_message + '\n'
                del deliverable.added_message
            call_and_response_details = f'{response_type} response\n{added_message}' \
                + f'verb {verb}, url {url}, kwargs {kwargs}\nresponse:\n{json.dumps(response_json, indent=4)}'
            logger.error(f'{call_and_response_details}\nStack trace: {"".join(traceback.format_list(raw_tb[:-1]))}')
    MainWindow.the().visual_log(f'{response_type} response {call_name}. See log.', logging.ERROR)
    
def check_keys(req_dict_structure, response_dict_structure, response):
    # so this function is about checking the keys in the returned json object...
    # NB both structures MUST be dicts
    if not isinstance(req_dict_structure, dict):
        response.added_message = f'req_dict_structure not dict: {type(req_dict_structure)}\n'
        return False
    if not isinstance(response_dict_structure, dict):
        response.added_message = f'response_dict_structure not dict: {type(response_dict_structure)}\n'
        return False
    for dict_key in req_dict_structure.keys():
        if dict_key not in response_dict_structure:
            response.added_message = f'key |{dict_key}| missing\n'
            return False
        req_value = req_dict_structure[dict_key]
        response_value = response_dict_structure[dict_key]
        if isinstance(req_value, dict):
            # if the response at this point is a list apply the req_value dict to each element:
            # failure in just one such element leads to "Anomalous response"... 
            if isinstance(response_value, list):
                for resp_list_element in response_value:
                    if not check_keys(req_value, resp_list_element, response):
                        return False
            elif not check_keys(req_value, response_value, response): # any other response value must be a dict (tested in next level of recursion)
                return False
        elif isinstance(req_value, list):
            if not isinstance(response_value, list): # if the req_value is a list the reponse must be one
                response.added_message = f'key |{dict_key}| not list: {type(response_value)}\n'
                return False
            # it is OK for the value to be a list, but these must be strings (keys) or dicts
            for req_list_element, resp_list_element in zip(req_value, response_value):
                if isinstance(req_list_element, dict):
                    if not check_keys(req_list_element, resp_list_element, response):
                        return False
                if not isinstance(req_list_element, str):
                    response.added_message = f'req_list_element not string: {type(req_list_element)}\n'
                    return False
                if req_list_element not in response_value:
                    response.added_message = f'key |{req_list_element}| missing from response list\n'
                    return False
        # put None as a dummy value (otherwise something like {'my_key'} will be seen as a set, not a dict 
        elif req_value != None: 
            response.added_message = f'required value of key |{dict_key}| must be None (dummy), dict or list: {type(req_value)}\n'
            return False
    return True

def process_json_requests_call(verb, url, **kwargs):
    # "call_name" is a mandatory kwarg
    if 'call_name' not in kwargs:
        raise Exception('kwarg "call_name" not supplied!')
    call_name = kwargs['call_name']
    del kwargs['call_name']

    required_keys = {}    
    if 'required_keys' in kwargs:
        required_keys = kwargs['required_keys']
        del kwargs['required_keys']

    acceptable_statuses = [200]
    if 'acceptable_statuses' in kwargs:
        acceptable_statuses = kwargs['acceptable_statuses']
        del kwargs['acceptable_statuses']

    exception_handler = log_response_error
    if 'exception_handler' in kwargs:
        exception_handler = kwargs['exception_handler']
        del kwargs['exception_handler']
        
    response, exception = requests_call(verb, url, **kwargs)

    if response == None:
        exception_handler('No', call_name, exception, verb, url, **kwargs)
        return (False, exception)
    try:
        response_json = response.json()
    except BaseException as e:
        logger.error(f'response.status_code {response.status_code} but calling json() raised exception')
        # an exception raised at this point can't truthfully lead to a "No response" message... so say "bad"
        exception_handler('Bad', call_name, e, verb, url, **kwargs)
        return (False, response)
        
    status_ok = response.status_code in acceptable_statuses
    if not status_ok:
        response.added_message = f'status code was {response.status_code}'
        log_response_error('Bad', call_name, response, verb, url, **kwargs)
        return (False, response)
    check_result = check_keys(required_keys, response_json, response)
    if not check_result:
        log_response_error('Anomalous', call_name, response, verb, url, **kwargs)
    return (check_result, response)      
4 Python module (a "thin wrapper" around requests). All the above is for illustration purposes.