23 juin 2024

[Dynamo += Python] Introduction Autodesk Plateforme Services (Partie 1/2)

  




Petite introduction a l'API Autodesk Plateforme Service directement (APS) depuis Dynamo (sur la plateforme ACC)

#DynamoBIM #Revit #Python  #ACC #APS    #AutodeskExpertElite #AutodeskCommunity 

if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )




Dans cet article, à l'aide d'APS, nous verrons  :
  1. comment enregistrer une WEB Application 
  2. comment s'authentifier depuis une application tierce (ici Dynamo) 
  3. un 1ᵉʳ exemple pour lister les utilisateurs des Projets ACC
Dans la suite de cet article, nous utiliserons Dynamo3 + IronPython3 et la librairie .Net Newtonsoft Json.

Vous trouverez dans un prochain article un exemple avec CPython3 et la libraire python json

  • Introduction

APS, ou Autodesk Platform Services, anciennement connu sous le nom de Forge, est une suite de services cloud offerte par Autodesk. Elle permet aux développeurs de créer des applications personnalisées, d'intégrer des flux de travail et d'automatiser des processus, grâce à plusieurs APIs bien documentées.




Nous utiliserons dans cet article les API suivantes 
    • Authentification API
    • Autodesk Construction Cloud Platform (ACC) API

  • Configuration initiale

Avant de pouvoir utiliser les APIs, APS, il est nécessaire d'enregistrer l'application pour obtenir un identifiant et un secret client. Pour plus d'informations, consultez ce tutoriel : Création d'une application APS

  • Authentification et Scopes

La connexion à APS, depuis Dynamo, se fait (dans cet exemple) via une authentification en deux étapes (2-Legged Token). Ce processus assure une sécurité renforcée pour l'accès aux APIs.

Nous devons également définir les 'scopes' suivant  : data:read  /  account:read


import sys
import clr
import System
from System.Collections.Generic import List, IList, Dictionary

import json
import urllib.request
import urllib.error
import json
import ssl
from base64 import b64encode

def get_auth_token(client_id, secret_id):
    url = "https://developer.api.autodesk.com/authentication/v2/token"
    client_credentials = f"{client_id}:{secret_id}"
    b_64_pass = b64encode(client_credentials.encode("utf-8")).decode("utf-8")
    #
    token = None
    payload = {
    "grant_type": "client_credentials", 
    "scope": "data:read data:write data:search account:read" ,
    }
    #
    # Content-Type is set to "application/x-www-form-urlencoded". For x-www-form-urlencoded, we need to URL-encode the data
    json_data = urllib.parse.urlencode(payload).encode('utf-8')
    
    # Create request with headers
    request = urllib.request.Request(
        url,
        data=json_data,
        headers={
            "Authorization": "Basic {}".format(b_64_pass),
            "Content-Type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
        },
        method='POST'
    )
    
    try:
        # Send request
        with urllib.request.urlopen(request) as response:
            status_code = response.getcode()
            response_data = json.loads(response.read().decode('utf-8'))
            token_type = response_data["token_type"]
            token = response_data["access_token"]
    #
    except urllib.error.HTTPError as e:
        print(f"Status Code: {e.code}")
        print(traceback.format_exc())
        try:
            error_data = json.loads(e.read().decode('utf-8'))
            print(f"Response: {error_data}")
        except:
            print(f"Response: Error {e.code} - {e.reason}")
    #
    except Exception as e:
        print(traceback.format_exc())
        
    return token

token = get_auth_token("CLIENT_ID_KEY", "CLIENT_SECRET_KEY")

OUT = token

  • Exemple : lister les utilisateurs par projet

Pour cela, nous utilisons la fonction suivante
https://aps.autodesk.com/en/docs/acc/v1/reference/http/admin-projectsprojectId-users-GET/

voici le code


# Load the Python Standard and DesignScript Libraries
import sys
import clr
import System
from System.Collections.Generic import List, IList, Dictionary

clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *


clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

my_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments)

import logging
import traceback
import time
import re
import json
import urllib.request
import urllib.error
import json
import ssl
from base64 import b64encode


def get_auth_token(client_id, secret_id):
    url = "https://developer.api.autodesk.com/authentication/v2/token"
    client_credentials = f"{client_id}:{secret_id}"
    b_64_pass = b64encode(client_credentials.encode("utf-8")).decode("utf-8")
    #
    token = None
    payload = {
    "grant_type": "client_credentials", 
    "scope": "data:read data:write data:search account:read" ,
    }
    #
    # Content-Type is set to "application/x-www-form-urlencoded". For x-www-form-urlencoded, we need to URL-encode the data
    json_data = urllib.parse.urlencode(payload).encode('utf-8')
    
    # Create request with headers
    request = urllib.request.Request(
        url,
        data=json_data,
        headers={
            "Authorization": "Basic {}".format(b_64_pass),
            "Content-Type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
        },
        method='POST'
    )
    
    try:
        # Send request
        with urllib.request.urlopen(request) as response:
            status_code = response.getcode()
            response_data = json.loads(response.read().decode('utf-8'))
            token_type = response_data["token_type"]
            token = response_data["access_token"]
    #
    except urllib.error.HTTPError as e:
        print(f"Status Code: {e.code}")
        print(traceback.format_exc())
        try:
            error_data = json.loads(e.read().decode('utf-8'))
            print(f"Response: {error_data}")
        except:
            print(f"Response: Error {e.code} - {e.reason}")
    #
    except Exception as e:
        print(traceback.format_exc())
        
    return token

def get_request_data_management(url):
    global token
    response_data = None
    # Prepare headers
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json" }

    # Build the request
    request = urllib.request.Request(url, headers=headers, method='GET' )
    try:
        # Send request
        with urllib.request.urlopen(request) as response:
            resultA = response.read().decode('utf-8')
            response_data = json.loads(resultA)
    except urllib.error.HTTPError as e:
        print(f"Status Code: {e.code}")
        try:
            error_data = json.loads(e.read().decode('utf-8'))
            print(f"Response: {error_data}")
        except:
            print(f"Response: Error {e.code} - {e.reason}")
    except Exception as e:
        print(traceback.format_exc())

    return response_data
    
def post_request_data_management(url, dict_content={}, dict_headers={}):
    global token
    response_data = None

    # Serialize to JSON
    jsonRequest = json.dumps(dict_content).encode('utf-8')
    # Set headers
    headers = dict_headers 
    # Prepare the POST request
    if dict_content:
        request = urllib.request.Request( url, data=jsonRequest, headers=headers,  method='POST' )
    else:
        request = urllib.request.Request( url, headers=headers,  method='POST' )

    try:
        # Send request and parse response
        with urllib.request.urlopen(request) as response:
            result = response.read().decode('utf-8')
            print("jsonResponse:", result)  
            response_data = json.loads(result)

    except urllib.error.HTTPError as e:
        print(f"Status Code: {e.code}")
        print(traceback.format_exc())
        try:
            error_data = json.loads(e.read().decode('utf-8'))
            print(f"Response: {error_data}")
        except:
            print(f"Response: Error {e.code} - {e.reason}")
    except Exception as e:
        print(traceback.format_exc())

    return response_data
    
out = []
dict_project_users = {}
my_hub_name = "MY_HUB"
# get token
token_type = None
token = get_auth_token("CLIENT_ID_KEY", "CLIENT_SECRET_KEY")
# get hub id
jobject_hubs_data = get_request_data_management("https://developer.api.autodesk.com/project/v1/hubs/")
data_hubs_array = jobject_hubs_data["data"]
my_hub_id = next((data["id"] for data in data_hubs_array if data["attributes"]["name"] == my_hub_name), None)
accountId = my_hub_id #re.sub(r"^b.", "", my_hub_id)
# get project id
jobject_projects_data = get_request_data_management(f"https://developer.api.autodesk.com/project/v1/hubs/{my_hub_id}/projects")
data_array_projects = jobject_projects_data["data"]
for data in data_array_projects:
    projectId = data["id"]
    projectName = data["attributes"]["name"]
    #
    dict_project_users[projectName] = []
    #
    j_project_users = get_request_data_management(f"https://developer.api.autodesk.com/construction/admin/v1/projects/{projectId}/users")
    if j_project_users is not None:
        data_array_users = j_project_users["results"]
        for user in data_array_users:
            email = user['email']
            name = user['name']
            role = ""
            for i in user["roles"]:
                role += i['name'] + ", "
    
            dict_project_users[projectName].append([email, name, role])

OUT = dict_project_users
.
  • Rajout de Paramètres a la requête (Query String Parameters)

Vous pouvez appliquer des filtres de requête grâce aux paramètres disponible avec cette fonction.
Voici un exemple ou filtre par email et l'on modifie le nombre maximum d'utilisateurs retournés.
Ici les filtres se mettent en fin d'URI suivant la syntaxe suivante :

URI?filtre1&filtre2


j_project_users = get_request_data_management("https://developer.api.autodesk.com/construction/admin/v1/projects/{projectId}/users?filter[email]=snee&filterTextMatch=contains&limit=50")
un autre exemple pour obtenir les métas-datas incluant le chemin

j_data_file = get_request_data_management(f"https://developer.api.autodesk.com/data/v1/projects/{my_project_id}/items/{file_id}?includePathInProject=true")
pathInProject = j_data_file["data"]["attributes"]["pathInProject"]
folder_name = pathInProject.split("/")[-1]


Voici une bonne documentation sur les conventions d'URI
https://www.odata.org/documentation/odata-version-2-0/uri-conventions/

  • Plus rapide ?
Vous pouvez également tenter d'utiliser du multi-threading, mais attention à la limite des quotas imposés par l'API APS


le même code avec des Threads 

# Load the Python Standard and DesignScript Libraries
import sys
import clr
import System
from System.Collections.Generic import List, IList, Dictionary
from System.Threading import Thread, ThreadStart

import traceback
import time
import re
import json
import urllib.request
import urllib.error
import json
import ssl
from base64 import b64encode

def get_auth_token(client_id, secret_id):
    url = "https://developer.api.autodesk.com/authentication/v2/token"
    client_credentials = f"{client_id}:{secret_id}"
    b_64_pass = b64encode(client_credentials.encode("utf-8")).decode("utf-8")
    #
    token = None
    payload = {
    "grant_type": "client_credentials", 
    "scope": "data:read data:write data:search account:read" ,
    }
    #
    # Content-Type is set to "application/x-www-form-urlencoded". For x-www-form-urlencoded, you need to URL-encode the data
    # URL encode the payload!
    json_data = urllib.parse.urlencode(payload).encode('utf-8')
    
    # Create request with headers
    request = urllib.request.Request(
        url,
        data=json_data,
        headers={
            "Authorization": "Basic {}".format(b_64_pass),
            "Content-Type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
        },
        method='POST'
    )
    
    try:
        # Send request
        with urllib.request.urlopen(request) as response:
            status_code = response.getcode()
            response_data = json.loads(response.read().decode('utf-8'))
            token_type = response_data["token_type"]
            token = response_data["access_token"]
    #
    except urllib.error.HTTPError as e:
        print(f"Status Code: {e.code}")
        print(traceback.format_exc())
        try:
            error_data = json.loads(e.read().decode('utf-8'))
            print(f"Response: {error_data}")
        except:
            print(f"Response: Error {e.code} - {e.reason}")
    #
    except Exception as e:
        print(traceback.format_exc())
        
    return token

def get_request_data_management(url):
    global token
    response_data = None
    # Prepare headers
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json" }

    # Build the request
    request = urllib.request.Request(url, headers=headers, method='GET' )
    try:
        # Send request
        with urllib.request.urlopen(request) as response:
            resultA = response.read().decode('utf-8')
            response_data = json.loads(resultA)
    except urllib.error.HTTPError as e:
        print(f"Status Code: {e.code}")
        try:
            error_data = json.loads(e.read().decode('utf-8'))
            print(f"Response: {error_data}")
        except:
            print(f"Response: Error {e.code} - {e.reason}")
    except Exception as e:
        print(traceback.format_exc())

    return response_data
    
def get_users_project(data):
    dict_users = {}
    projectId = data["id"]
    projectName = data["attributes"]["name"]
    #
    dict_users[projectName] = []
    #
    j_project_users = get_request_data_management(f"https://developer.api.autodesk.com/construction/admin/v1/projects/{projectId}/users")
    if j_project_users is not None:
        data_array_users = j_project_users["results"]
        for user in data_array_users:
            email = user['email']
            name = user['name']
            role = ""
            for i in user["roles"]:
                role += i['name'] + ", "
            dict_users[projectName].append([email, name, role])
    return dict_users
    
class Worker():
    __slots__ = 'fn', 'args', 'result'
    def __init__(self, fn, args):
        self.fn = fn
        self.args = args
        self.result = None
    def __call__(self):
        self.result = self.fn(*self.args)
        
start = time.time()
out = []
my_hub_name = "MY_HUB"
# get token
token_type = None
token = get_auth_token("CLIENT_ID_KEY", "CLIENT_SECRET_KEY")
# get hub id
jobject_hubs_data = get_request_data_management("https://developer.api.autodesk.com/project/v1/hubs/")
data_hubs_array = jobject_hubs_data["data"]
my_hub_id = next((data["id"] for data in data_hubs_array if data["attributes"]["name"] == my_hub_name), None)
accountId = my_hub_id #re.sub(r"^b.", "", my_hub_id)
# get project id
jobject_projects_data = get_request_data_management(f"https://developer.api.autodesk.com/project/v1/hubs/{my_hub_id}/projects")
data_array_projects = jobject_projects_data["data"]
# set workers
workers, tasks = [], []
for data in data_array_projects:
    w = Worker(get_users_project, (data,))
    t = Thread(ThreadStart(w))
    workers.append(w)
    tasks.append(t)
    t.Start()

for t in tasks: t.Join() 

# merge all dict
out_dict = {}
for x in workers:
    out_dict = dict(out_dict, **x.result)
elapsed = time.time() - start
OUT = elapsed, out_dict





Dans un prochain article, nous verrons un exemple un peu plus complexe (recherche de fichiers au sein d'un projet).




« Il vaut mieux être un optimiste qui se trompe parfois qu'un pessimiste qui a toujours raison. »
Mark Twain

0 commentaires:

Enregistrer un commentaire