<< Back

Upload sur Tableau Server en Python


Bonjour à tous ! Le tutoriel du jour visera à écrire un petit script en Python pour uploader vos Dashboards ou Datasources sur Tableau Server. L’idée est de compléter le tutoriel des filtres dynamiques Tableau en python. Nous utiliserons l’API REST de Tableau Server . Nous écrirons du code Python 2.7. Ce post nécessitera de bonnes connaissances en programmation, alors j’espère que vous avez pris un bon petit déjeuner car ça va être chaud !

C’est parti !

1/ Identification

Si vous avez déjà été confrontés à l’utilisation de l’API REST de Tableau Server vous savez qu’il vous faut « incarner » un utilisateur en vous identifiant auparavant. Cela implique que vous devez avoir un utilisateur administrateur sous la main (mot de passe et identifiant compris). Faites donc bien attention à enregistrer vos identifiants à un endroit accessible de vous seul. Je vais enregistrer les identifiants dans les variables d’environnement de ma machine. TABLEAU_SERVER sera l’url de mon serveur, TABLEAU_USER correspondra au nom d’utilisateur administrateur et TABLEAU_PWD son mot de passe. Par ailleurs, je vais avoir besoin de la version de l’API Tableau Server utilisée (à l’heure où j’écris ces lignes nous sommes en 2.8) ainsi que le namespace correspondant.

1.1/ imports et variables d’environnement

Nous avons besoin des librairies os pour les variables d’environnement, ElementTree pour le parsing XML, ainsi que la librairie requests pour nos calls HTTP.
import os, requests
import xml.etree.ElementTree as ET
    
TABLEAU_SERVER = os.environ.get('TABLEAU_SERVER')
TABLEAU_USER = os.environ.get('TABLEAU_USER')
TABLEAU_PWD = os.environ.get('TABLEAU_PWD')
TABLEAU_SITE = os.environ.get('TABLEAU_SITE')
TABLEAU_VERSION = '2.8'
TABLEAU_NAMESPACE = {'t': 'http://tableau.com/api'}

Avec cette approche, il vous faudra enregistrer ces paramètres dans vos variables d’environnement que ce soit sur Windows, Linux ou MacOS.

1.2/ Login

Je vais travailler en XML pour ce tutoriel, mais vous êtes libres de travailler en JSON si vous le souhaitez. D’après la documentation Tableau, il nous faut construire un objet XML avec la structure suivante :
<tsRequest>
    <credentials name="username" password="password">
        <site contentUrl="content-url" />
    </credentials>
</tsRequest>

Nous allons donc écrire une méthode login qui se contentera de construire l’objet XML ci-dessus à l’aide de la librairie ElementTree.
#Login to Tableau et récupère token et site_id
def login():
    url = "{0}/api/{1}/auth/signin".format(TABLEAU_SERVER, TABLEAU_VERSION)
    
    #1/ Création du payload XML
    tsRequest = ET.Element('tsRequest')
    credentials = ET.SubElement(tsRequest, 'credentials', name=TABLEAU_USER, password=TABLEAU_PWD)
    ET.SubElement(credentials, 'site', contentUrl=TABLEAU_SITE)
    body = ET.tostring(tsRequest)
    
    #2/ Requête HTTP POST
    response = requests.post(url, data=body,verify=False)
    if response.status_code != 200:
        raise Exception("Login Error")
    
    #3/ Chargement du contenu dans un arbre XML
    response = ET.fromstring(response.text)
    
    #4/ Récupération du token et du site_id
    token = response.find('t:credentials', namespaces=TABLEAU_NAMESPACE).get('token')
    site_id = response.find('.//t:site', namespaces=TABLEAU_NAMESPACE).get('id')
        return token, site_id

NB: verify=False signifie que l’on ne cherchera pas à vérifier le certificat SSL de notre serveur.

1.3 Logout

Tant que nous y sommes, écrivons la même chose pour la méthode logout :
#Déconnexion du Server
def logout(token):
    url = "{0}/api/{1}/auth/signin".format(TABLEAU_SERVER, TABLEAU_VERSION)
    response = requests.post(url, headers={'x-tableau-auth':token},verify=False)
    if response.status_code != 204
        raise Exception("Logout Error")
    return

Notez que la requête logout prend en header le paramètre « X-Tableau-Auth » contenant le token. Ce header doit être spécifié à chaque fois que l’on fera une requête authentifiée.

2/ Makemultipart & chunk data

Il est courant de travailler avec des fichiers volumineux dans Tableau. Ces fichiers volumineux peuvent concerner les datasources tout comme les dashboards par ailleurs. Or, cela peut devenir un problème lorsque l’on cherche à uploader ces fichiers via des requêtes à une API. Plus le fichier est volumineux, plus le transfert prendra du temps et plus les chances d’avoir un problème lors du transfert augmentent. Par ailleurs, l’API Tableau limite à 64MB la taille des requêtes. Il est donc nécessaire de « chunker » nos requêtes en une multitude de petits morceaux.

Je vous invite à lire cette partie de la documentation. Vous verrez qu’il y a deux manières d’uploader un fichier : en une seule requête ou bien en une multitude de petites requêtes. En ce qui me concerne, je trouve plus intéressant de traiter le cas du chunking. En effet, cette méthode a beau être plus complexe, elle fonctionnera quelque soit la taille de votre fichier.

La documentation nous indique que la publication d’un fichier doit se faire en trois partie : l’initialisation, l’upload et la publication.

2.1/ Initialisation de l’upload

L’initialisation de l’upload consiste à récupérer un identifiant unique de session qui sera passé dans chaque requête pour indiquer au serveur de quel upload il s’agit. Voici cette méthode :
def getSessionId(token, site_id):
    url = "{0}/api/{1}/sites/{2}/fileUploads".format(TABLEAU_SERVER, TABLEAU_VERSION, site_id)
    response = requests.post(url, headers={'x-tableau-auth': token},verify=False)
    if response.status_code != 201:
        raise Exception("Get Session Id Error")
    response = ET.fromstring(response.text).find('t:fileUpload', namespaces=TABLEAU_NAMESPACE).get('uploadSessionId')
    return response

2.2/ Upload du fichier en morceaux

C’est ici que les choses sont délicates. La documentation Tableau indique que nous devons construire une requête avec un Content-Type égal à « multipart/mixed ». L’idée est d’avoir une partie de la requête au format « text/xml » et l’autre partie au format « application/octet-stream ». Typiquement, la première partie de la requête pourra contenir des informations spécifiques au fichier alors que l’autre partie contiendra le chunk lui-même au format binaire.

NB: D’après la documentation, pour l’upload en morceaux, la première partie de la requête sera toujours vide car les données spécifiques au fichier sont spécifiées à l’étape suivante 2.3 (la publication).

L’idée est donc de lire notre fichier petit à petit en morceaux d’une taille que j’ai fixée à 1MB. Le code qui suit contient donc la méthode upload ainsi que la méthode de création de payload et content-type à l’aide de la librairie requests de Python. La méthode createChunk utilise les fonctions RequestField, make_multipart et encode_multipart_formdata de la librairie requests pour construire le payload. On doit lui passer un objet contenant filename, data et content-type pour chaque section de la requête. Dans notre cas, il y a deux sections, une section request_payload au format text/xml et une section tableau_file au format application/octet-stream.

La méthode upload lit le fichier spécifié par morceaux d’une taille de 1MB et génére autant d’appels PUT à l’API qu’il y a de chunks pour le fichier concerné.

#Création d'un payload à partir d'un chunk
def createChunk(obj):
    morceaux = []
    for key, value in obj.iteritems():
        morceau = requests.packages.urllib3.fields.RequestField(name=key, data=value["data"], filename=value["filename"])
        morceau.make_multipart(content_type=value["content-type"])
        morceaux.append(morceau)
    
    body, content_type = requests.packages.urllib3.filepost.encode_multipart_formdata(morceaux)
    content_type = ''.join(('multipart/mixed',) + content_type.partition(';')[1:])
    return body, content_type
    
#Upload des données
def upload(token, site_id, session_id, filename, ext, type):
    chunk = 1048576 #1MB (à changer si vous souhaitez augmenter ou diminuer les chunks)
    url = "{0}/api/{1}/sites/{2}/fileUploads/{3}".format(TABLEAU_SERVER, TABLEAU_VERSION, site_id, session_id)
    
    #lecture du fichier
    continuer = True
    with open("{0}.{1}".format(filename, ext), 'rb') as f:
        #upload des données tant qu'il y en reste...
        while continuer:
            data = f.read(chunk)
            if data:
                body, content_type = createChunk({'request_payload': {'filename' : '', 'data' : '', 'content-type' :'text/xml'}, 'tableau_file': {'filename' : 'file', 'data':data, 'content-type' : 'application/octet-stream'}})
                response = requests.put(url, data=body, headers={'x-tableau-auth': token, "content-type": content_type}, verify=False)
                if response.status_code != 200:
                    raise Exception("Put Request Error")
            else:
                continuer = False

Cette portion du code est délicate mais vous pouvez tout à fait copier-coller ces deux méthodes sans réfléchir à ce qu’elles font ! 🙂

2.3/ Publication du fichier

Une fois le fichier uploadé, il ne nous reste plus qu’à le publier. C’est à ce moment que nous passons les informations concernant le fichier lui-même. Les informations dont nous avons besoin sont :

  • id du projet où publier
  • le nom du fichier
  • le type de fichier (workbook ou datasource)
  • l’extension (hyper, tde, twb)

#Méthode pour terminer l'upload et publier le fichier
def publish(token, site_id, session_id, project_id, filename, type, extension):
    #Construction du Payload
    tsRequest = ET.Element('tsRequest')
    element = ET.SubElement(tsRequest, type, name=filename)
    ET.SubElement(element, 'project', id=project_id)
    body = ET.tostring(tsRequest)
    
    url = "{0}/api/{1}/sites/{2}/{3}s?uploadSessionId={4}&{5}Type={6}&overwrite=true".format(TABLEAU_SERVER, TABLEAU_VERSION, site_id, type,
    session_id, type, extension)
    
    body, content_type = createChunk({'request_payload': {'filename' : '', 'data': body, 'content-type' :'text/xml'}})
    response = requests.post(url, data=body, headers={'x-tableau-auth': token, 'content-type': content_type},verify=False)
    if response.status_code != 201:
        raise Exception("Erreur de publication")
    return

3/ Conclusion

Si vous êtes arrivés jusqu’ici, bravo ! Sans rentrer dans plus de détails, il ne nous reste plus qu’à assembler les méthodes énoncées ci-dessus :

def main(filename):
    filename, ext = filename.split('.', 1)
    
    if ext == "hyper" or ext == "tde":
        type = "datasource"
    elif ext == "twb":
        type = "workbook"
    else:
        raise Exception("Extension {0} non prise en compte!".format(ext))
    
    #Login
    token, site_id = login()
    
    #Récupération d'un projet au hasard (pour les besoins du tutoriel)
    project_id = getProjectId(token, site_id)
    
    #Initialisation d'une Session
    session_id = getSessionId(token, site_id)
    
    #Upload du fichier
    upload(token, site_id, session_id, filename, ext, type)
    
    #Publication du fichier
    publish(token, site_id, session_id, project_id, filename, type, ext)
    
    #Déconnexion
    logout(token)
    
if __name__ == '__main__':
    main("mon_workbook.hyper")

Notez que je n’ai pas pris la peine de décrire la manière dont vous pouvez récupérer le project_id. Ce sera à vous d’écrire cette méthode suivant vos besoins ;). Le script en entier fait un peu moins de 150 lignes de code. Comme ce tutoriel est déjà très long, je ne vais pas mettre le script en entier mais vous pouvez toujours le télécharger ici!

Le sujet de ce tutoriel visait à montrer comment utiliser l’API Tableau Server en Python pour uploader vos workbooks et datasources automatiquement d’après le projet Github Officiel Tableau.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *