Search code examples
pythonazureazure-devopsazure-devops-rest-api

Azure Devops: CFD widget getting an error in the web interface after API call is made


I've been writing some Python code to programatically update the CFD widget settings, to display data for the last 14, 15, 16 days and so on. But, as I was testing the API endpoint to update widgets, the web interface throws me an error:

TypeError: e.split is not a function

However, the API request I made got a successful return. Here's the code:

import fazer_requisicoes
import json
import base64
import os
from dotenv import load_dotenv
from pprint import pp

load_dotenv()

token_do_azure = os.environ['AZURE_DEVOPS_PAT']

pat_encoded = base64.b64encode((":" + token_do_azure).encode()).decode()
headers = {
    'Content-Type': 'application/json',
    'Authorization': f'Basic {pat_encoded}'
}

settings_dict = {
    "chartDataSettings": {
        "timePeriod": {
            "settings": 16
        }
    }
}

def atualizar_cfd():
    organization = 'dummy'
    project = 'dummy'
    team = 'dummy'
    dashboard_id = 'dummy'
    widget_id = 'dummy'
    url = f'https://dev.azure.com/{organization}/{project}/{team}/_apis/dashboard/dashboards/{dashboard_id}/widgets/{widget_id}?api-version=7.2-preview.2'
    get_req = fazer_requisicoes.fazer_requisicoes("GET", url, headers)
    get_req_text = json.loads(get_req.text)
    # Assuming the ETag is in the body, otherwise look for it in the headers
    current_etag_widget = get_req_text['eTag']
    current_etag_dashboard = get_req_text['dashboard']['eTag']
    pp(current_etag_widget)
    pp(current_etag_dashboard)
    url = f'https://dev.azure.com/{organization}/{project}/{team}/_apis/dashboard/dashboards/{dashboard_id}/widgets/{widget_id}?api-version=7.2-preview.2'
    req_body = {
        "id": widget_id,
        "name": "Cumulative Flow Diagram (CFD)",
        "eTag": f'{current_etag_widget}',
        'position': {'row': 4, 'column': 5},
        "size": {
            "rowSpan": 2, "columnSpan": 3
        },
        'settings': settings_dict,
        "settingsVersion":
        {
            "major": 1,
            "minor": 0,
            "patch": 0
        },
        "contributionId": "ms.vss-dashboards-web.Microsoft.VisualStudioOnline.Dashboards.CumulativeFlowDiagramWidget",
        'dashboard': {'position': -1,
                      'eTag': f'{current_etag_dashboard}',
                      'modifiedDate': '0001-01-01T00:00:00',
                      'lastAccessedDate': '0001-01-01T00:00:00'},
    }
    req = fazer_requisicoes.fazer_requisicoes("PATCH", url, headers, req_body)
    req_text = json.loads(req.text)
    req_response = json.dumps(req_text)
    pp(req_response)


def main():
    atualizar_cfd()


main()

Here's the 'fazer_requisicoes function:

import requests
import time
import urllib3
from requests.exceptions import JSONDecodeError
from pprint import pprint

urllib3.disable_warnings()

session = requests.Session()

contador_timeout = 0
contador_SSL = 0

def fazer_requisicoes(metodo, url, headers, request_body=None):
    global contador_timeout
    global contador_SSL

    while True:
        try:
            if metodo in ['GET', 'DELETE']:
                req = session.request(metodo, url, headers=headers, verify=False)
            else:
                req = session.request(metodo, url, headers=headers, json=request_body, verify=False)
                try:
                    req.json()
                except JSONDecodeError:
                    print(f'{req.status_code}\n')
                    pprint(req.text)
                    print(f"Resposta inválida recebida. Tentando novamente...")
                    time.sleep(10)
                    continue 
            return req
        except requests.exceptions.ConnectTimeout as e:
            contador_timeout += 1
            print(f"Erro de timeout número {contador_timeout}. Tentando novamente...")
            time.sleep(10)
        except requests.exceptions.SSLError as e:
            contador_SSL += 1
            print(f"Erro de SSL número {contador_SSL}. Tentando novamente...")
            time.sleep(10)

Can I fix it? If yes, how?

I've tried to use multilinein the settings, but it throwed another error in the web interface, a SyntaxError.


Solution

  • You issue may be related to the following things:

    1. The value of "settings" object is a json string in which the all the double quotes (") are escaped by slash (\). When passing to the PATCH API call, also should use the escaped json string.

    2. When updating the value of "settings" object, it seems needs to pass the whole escaped json string with the updated part instead of only the updated part to the PATCH API call.

    Below is a Python sample I attempt on my side. It can work well to update the settings of CFD Widget without any error. You can reference it to update your Python script.

    # coding=utf-8
    
    import os
    import json
    import base64
    import requests
    
    organization = '{organization}'
    project = '{project}'
    team = '{team}'
    dashboardId = '{dashboardId}'
    widgetId = '{widgetId}'
    
    ado_pat = os.environ['AZURE_DEVOPS_PAT']
    b64token = base64.b64encode((":" + ado_pat).encode()).decode()
    
    url = f'https://dev.azure.com/{organization}/{project}/{team}/_apis/dashboard/dashboards/{dashboardId}/widgets/{widgetId}?api-version=7.2-preview.2'
    
    headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': f'Basic {b64token}'
    }
    
    session = requests.Session()
    
    print("-------------------- CFD Widget befor updating --------------------")
    response_get = session.request('GET', url, headers=headers, json=None)
    json_object_get = json.loads(response_get.content)
    json_format_get = json.dumps(json_object_get, indent=2)
    print(json_format_get)
    
    print("-------------------- Updating CFD Widget --------------------")
    body_patch = json_object_get
    
    print("The settings before updating is:")
    settings = body_patch["settings"]
    json_object_settings = json.loads(settings)
    json_format_settings = json.dumps(json_object_settings, indent=2)
    print(json_format_settings)
    
    print("Update the settings to:")
    json_object_settings["chartDataSettings"]["timePeriod"]["settings"] = 20
    json_format_settings_updated = json.dumps(json_object_settings, indent=2)
    print(json_format_settings_updated)
    
    print("-------------------- CFD Widget after updating --------------------")
    body_patch["settings"] = json.dumps(json_object_settings).replace('"', '\"')
    response_patch = session.request('PATCH', url, headers=headers, json=body_patch)
    json_object_patch = json.loads(response_patch.content)
    json_format_patch = json.dumps(json_object_patch, indent=2)
    print(json_format_patch)