Search code examples
pythonpython-requestssmartsheet-api

Smartsheet API in Python: how to access response data from client


I am trying to understand / plan for the rate limits, particularly for the get_cell_history method in the python api.

When I run the code below, I see the following response printed to my console:

{"response": {"statusCode": 429, "reason": "Too Many Requests", "content": {"errorCode": 4003, "message": "Rate limit exceeded.", "refId": "SOME_REF_ID"}}}

But I am tyring to force my if statement to read the response and check for the 429 status code

import smartsheet

smartsheet_client = smartsheet.Smartsheet('My_API_KEY')
smartsheet_client.errors_as_exceptions(True)

sheet_id = 'My_Sheet_ID'
edit_sheet = smartsheet_client.Sheets.get_sheet(sheet_id)  # type: smartsheet.smartsheet.models.Sheet

for row in edit_sheet.rows:  # type: smartsheet.smartsheet.models.Row
    for cell in row.cells:  # type: smartsheet.smartsheet.models.Cell
        cell_history = smartsheet_client.Cells.get_cell_history(sheet_id, row.id, cell.column_id,
                                                                include_all=True)
        if cell_history.request_response.status_code == 429:
            print(f'Rate limit exceeded.')
        elif cell_history.request_response.status_code == 200:
            print(f'Found Cell History: {len(cell_history.data)} edits')

Where can I access this response? Why does my program run without printing out the "Rate limit exceeded" string?

I am using Python 3.8 and the package smartsheet-python-sdk==2.105.1


Solution

  • The following code shows how to successfully catch an API error when using the Smartsheet Python SDK. For simplicity, I've just used the Get Sheet operation and handled the error with status code 404 in this example, but you should be able to implement the same approach in your code. Note that you need to include the smartsheet_client.errors_as_exceptions() line (as shown in this example) so that Smartsheet errors get raised as exceptions.

    # raise Smartsheet errors as exceptions
    smartsheet_client.errors_as_exceptions()
    
    try:
        # call the Get Sheet operation
        # if the request is successful, my_sheet contains the Sheet object
        my_sheet = smartsheet_client.Sheets.get_sheet(4183817838716804) 
    except smartsheet.exceptions.SmartsheetException as e:
        # handle the API error
        if isinstance(e, smartsheet.exceptions.ApiError):
            if (e.error.result.status_code == 404):
                print(f'Sheet not found.')
            else:
                print(f'Another type of error occurred: ' + str(e.error.result.status_code))
    

    Finally, here's a bit of additional info about error handling with the Smartsheet Python SDK that may be helpful (from this other SO thread).

    • SmartsheetException is the base class for all of the exceptions raised by the SDK. The two most common types of SmartsheetException are ApiError and HttpError. After trapping the exception, first determine the exception type using an isinstance test.

    • The ApiError exception has an Error class object accessible through the error property, and then that in turn points to an ErrorResult class accessible through the result property.

    • The details of the API error are stored in that ErrorResult.

    • Note that to make the Python SDK raise exceptions for API errors, you must call the errors_as_exceptions() method on the client object.

    ---------------------

    UPDATE (retry logic):

    To repro the behavior you've described in your first comment below, I used (most of) the code from your original post above, and added some print statements to it. My code is as follows:

    # raise Smartsheet errors as exceptions
    smartsheet_client.errors_as_exceptions()
    
    sheet_id = 5551639177258884
    edit_sheet = smartsheet_client.Sheets.get_sheet(sheet_id)  # type: smartsheet.smartsheet.models.Sheet
    
    # initialize ctr
    row_ctr = 0
    
    for row in edit_sheet.rows:  # type: smartsheet.smartsheet.models.Row
        row_ctr += 1
        print('----ROW # ' + str(row_ctr) + '----')
        cell_ctr = 0
        for cell in row.cells:  # type: smartsheet.smartsheet.models.Cell
            cell_ctr += 1
            try:
                cell_history = smartsheet_client.Cells.get_cell_history(sheet_id, row.id, cell.column_id, include_all=True)
                print(f'FOUND CELL HISTORY: {len(cell_history.data)} edits [cell #' + str(cell_ctr) + ' ... rowID|columnID= ' + str(row.id) + '|' + str(cell.column_id) + ']')
            except smartsheet.exceptions.SmartsheetException as e:
                # handle the exception
                if isinstance(e, smartsheet.exceptions.ApiError):
                    if (e.error.result.status_code == 429):
                        print(f'RATE LIMIT EXCEEDED! [cell #' + str(cell_ctr) + ']')
    

    Interestingly, it seems that the Smartsheet Python SDK has built-in retry logic (with exponential backoff). The exception handling portion of my code isn't ever reached -- because the SDK is recognizing the Rate Limiting error and automatically retrying (with exponential backoff) any requests that fail with that error, instead of raising an exception for my code to catch.

    I can see this happening in my print output. For example, here's the cell history output from Row #2 in my sheet (each row has 8 columns / cells) -- where all Get Cell History calls were processed without any exceptions being thrown.

    row #2 output

    Things continue without error until the 7th row is being processed, when a Rate Limiting error occurs when it's trying to get cell history for the fifth column/cell in that row. Instead of this being a fatal error though -- I see processing pause for a few seconds (i.e., no additional output is printed to my console for a few seconds)...and then processing resumes. Another Rate Limiting error occurs, followed by a slightly longer pause in processing, after which processing resumes and cell history is successfully retrieved for the fifth through eighth cells in that row.

    row #7 output

    As my program continues to run, I see this same thing happen several times -- i.e., several API calls are successfully issued, then a Rate Limiting error occurs, processing pauses momentarily, and then processing resumes with API calls successfully issued, until another Rate Limiting error occurs, etc. etc. etc.

    To confirm that the SDK is automatically retrying requests that fail with the Rate Limiting error, I dug into the code a bit. Sure enough, I see that the request_with_retry function within /smartsheet/smartsheet.py does indeed include logic to retry failing requests with exponential backoff (if should_retry is true).

    request_with_retry

    Which begs the question, how does should_retry get set (i.e., in response to which error codes will the SDK automatically retry a failing request)? Once again, the answer is found within /smartsheet/smartsheet.py (SDK source code). This suggests that you don't need exception handling logic in your code for the Rate Limiting error (or for any of the other 3 errors where should_retry is set to True), as the SDK is built to automatically retry any requests that fail with that error.

    error codes to retry