Utiliser des données Dataverse à l’aide du Kit de développement logiciel (SDK) Python

Cet article illustre un exemple de code qui utilise le Kit de développement logiciel (SDK) pour utiliser des données et des métadonnées Dataverse. Avant de continuer, veillez à lire Bien démarrer.

Opérations de base

Voici un exemple de code qui fonctionne sur la table de compte.

from azure.identity import InteractiveBrowserCredential
from PowerPlatform.Dataverse.client import DataverseClient

# Replace <myorg> with the name of a valid environment.
base_url = "https://<myorg>.crm.dynamics.com"
client = DataverseClient(base_url=base_url, credential=InteractiveBrowserCredential())

# Create a record
account_id = client.records.create("account", {"name": "Contoso Ltd"})

# Read a record
account = client.records.retrieve("account", account_id)
print(account["name"])

# Read with expand fetches a related record in the same HTTP request
account = client.records.retrieve(
    "account", account_id,
    select=["name"],
    expand=["primarycontactid"],
)
contact = (account.get("primarycontactid") or {})
print(contact.get("fullname"))

# Update a record
client.records.update("account", account_id, {"telephone1": "555-0199"})

# Delete a record
client.records.delete("account", account_id)

Gestionnaire de contexte

Le Gestionnaire de contexte gère le nettoyage automatique et le regroupement de connexions HTTP. Utilisez la syntaxe suivante pour tirer parti du Gestionnaire de contexte.

with DataverseClient("https://<myorg>.crm.dynamics.com", credential) as client:

Le code de travail suivant montre comment utiliser le Gestionnaire de contextes.

from azure.identity import InteractiveBrowserCredential
from PowerPlatform.Dataverse.client import DataverseClient

# Connect to Dataverse
credential = InteractiveBrowserCredential()

with DataverseClient("https://<myorg>.crm.dynamics.com", credential) as client:

    # Create a contact
    contact_id = client.records.create("contact", {"firstname": "John", "lastname": "Doe"})

    # Read the contact back
    contact = client.records.retrieve("contact", contact_id, select=["firstname", "lastname"])
    print(f"Created: {contact['firstname']} {contact['lastname']}")

    # Clean up
    client.records.delete("contact", contact_id)

# Session closed, caches cleared automatically

Opérations en bloc

Voici quelques exemples qui effectuent des opérations en bloc.

# Bulk create
payloads = [
    {"name": "Company A"},
    {"name": "Company B"},
    {"name": "Company C"}
]
ids = client.records.create("account", payloads)

# Bulk update (broadcast same change to all)
client.records.update("account", ids, {"industry": "Technology"})

# Bulk delete
client.records.delete("account", ids, use_bulk_delete=True)

L’exemple suivant crée plusieurs comptes. Transmettez une liste de charges utiles à create(logical_name, payloads) pour appeler l'action liée à la collection Microsoft.Dynamics.CRM.CreateMultiple. La méthode retourne list[str] des ID d’enregistrement créés.

# Bulk create accounts (returns list of GUIDs)
payloads = [
    {"name": "Contoso"},
    {"name": "Fabrikam"},
    {"name": "Northwind"},
]
ids = client.records.create("account", payloads)
assert isinstance(ids, list) and all(isinstance(x, str) for x in ids)
print({"created_ids": ids})

Pour plus d’informations sur les opérations en bloc :

  • update retourne None pour les appels à enregistrement unique et en bloc, afin de maintenir la cohérence sémantique.
  • Le mode diffusion ou par enregistrement est déterminé selon que le paramètre changes est un dictionnaire ou une liste.
  • L’attribut de clé primaire est ajouté automatiquement lors de la création des cibles de l’action UpdateMultiple.
  • Si une charge utile omet @odata.type, le SDK l’étiquette automatiquement (la recherche de nom logique mise en cache).
  • La réponse inclut uniquement les ID : le KIT de développement logiciel (SDK) retourne ces chaînes GUID.
  • create retourne le GUID du nouvel enregistrement sous la forme d’un str lorsque vous passez un seul dictionnaire, et sous la forme d’un list[str] lorsque vous passez une liste de charges utiles.
  • La recherche @odata.type de métadonnées est effectuée une fois par jeu d’entités (mis en cache en mémoire).

Upsert (création et mise à jour)

Une séquence d’accès aux données courante consiste à vérifier d’abord si une ligne de table existe. Si la ligne existe, mettez-la à jour. Sinon, créez la ligne. Vous pouvez rendre cette séquence plus efficace en utilisant un seul appel d’API de l’opération Upsert.

Pour plus d’informations, consultez Utiliser Upsert pour créer ou mettre à jour un enregistrement.

Important

La table doit avoir une autre clé configurée dans Dataverse pour les colonnes utilisées dans alternate_key. Définissez d'autres clés dans les métadonnées de la table via le portail Power Apps maker ou un appel d'API Dataverse. Sans clé secondaire configurée, Dataverse rejette les requêtes upsert avec une erreur 400.

Permet client.records.upsert() de créer ou de mettre à jour des enregistrements identifiés par d’autres clés. Lorsque la clé correspond à un enregistrement existant, la méthode met à jour l’enregistrement. Sinon, il crée l’enregistrement. Un seul élément utilise une requête PATCH tandis que plusieurs éléments utilisent l’action UpsertMultiple en bloc.

from PowerPlatform.Dataverse.models import UpsertItem

# Upsert a single record
client.records.upsert("account", [
    UpsertItem(
        alternate_key={"accountnumber": "ACC-001"},
        record={"name": "Contoso Ltd", "telephone1": "555-0100"},
    )
])

# Upsert multiple records (uses UpsertMultiple bulk action)
client.records.upsert("account", [
    UpsertItem(
        alternate_key={"accountnumber": "ACC-001"},
        record={"name": "Contoso Ltd"},
    ),
    UpsertItem(
        alternate_key={"accountnumber": "ACC-002"},
        record={"name": "Fabrikam Inc"},
    ),
])

# Composite alternate key (multiple columns identify the record)
client.records.upsert("account", [
    UpsertItem(
        alternate_key={"accountnumber": "ACC-001", "address1_postalcode": "98052"},
        record={"name": "Contoso Ltd"},
    )
])

# Plain dict syntax (no import needed)
client.records.upsert("account", [
    {
        "alternate_key": {"accountnumber": "ACC-001"},
        "record": {"name": "Contoso Ltd"},
    }
])

Cadres de données

Le SDK fournit des wrappers Pandas pour toutes les opérations CRUD via l’espace de noms client.dataframe. Ces wrappers utilisent les API DataFrame et Series de pandas pour les entrées et les sorties.

Note

client.dataframe.get() est déconseillé. Utilisez les modèles en disponibilité générale indiqués dans les sections suivantes.

import pandas as pd
from PowerPlatform.Dataverse.models import col

# Query records as a single DataFrame (GA builder pattern)
df = (client.query.builder("account")
      .select("name", "telephone1")
      .where(col("statecode") == 0)
      .execute()
      .to_dataframe())
print(f"Found {len(df)} accounts")

# Limit results with top for large tables
df = client.query.builder("account").select("name").top(100).execute().to_dataframe()

# Create records from a DataFrame (returns a Series of GUIDs)
new_accounts = pd.DataFrame([
    {"name": "Contoso", "telephone1": "555-0100"},
    {"name": "Fabrikam", "telephone1": "555-0200"},
])
new_accounts["accountid"] = client.dataframe.create("account", new_accounts)

# Update records from a DataFrame (id_column identifies the GUID column)
new_accounts["telephone1"] = ["555-0199", "555-0299"]
client.dataframe.update("account", new_accounts, id_column="accountid")

# Clear a field by setting clear_nulls=True (by default, NaN/None fields are skipped)
df = pd.DataFrame([{"accountid": new_accounts["accountid"].iloc[0], "websiteurl": None}])
client.dataframe.update("account", df, id_column="accountid", clear_nulls=True)

# Delete records by passing a Series of GUIDs
client.dataframe.delete("account", new_accounts["accountid"])

# SQL query directly to DataFrame (supports JOINs, aggregates, GROUP BY)
df = client.dataframe.sql(
    "SELECT a.name, COUNT(c.contactid) as contacts "
    "FROM account a "
    "JOIN contact c ON a.accountid = c.parentcustomerid "
    "GROUP BY a.name"
)

Charger des fichiers dans Dataverse

L’exemple suivant montre comment charger un fichier nommé document.pdf dans la colonne Fichier nommée new_Document d’un enregistrement de compte. Le Kit de développement logiciel (SDK) pour Python gère automatiquement la segmentation des fichiers pour les fichiers de plus de 128 Mo.

# Upload a file to a record
client.files.upload(
    "account",
    account_id,
    "new_Document",
    "/path/to/document.pdf",
)

Important

Si la colonne de fichier n’existe pas, le Kit de développement logiciel (SDK) le crée automatiquement. La création d’une colonne est une modification des métadonnées : vous avez besoin de privilèges pour personnaliser la table et la nouvelle colonne devient une partie permanente du schéma de votre environnement. Dans les environnements de production, créez des colonnes de fichiers à l’avance plutôt que de vous appuyer sur la création automatique.

La upload méthode accepte les paramètres facultatifs pour contrôler le type de contenu et la stratégie de transfert.

# Specify a MIME type (defaults to application/octet-stream)
client.files.upload(
    "account", account_id, "new_Contract", "/path/to/contract.pdf",
    mime_type="application/pdf",
)

# Force chunked transfer regardless of size
client.files.upload(
    "email", email_id, "new_Attachment", "/path/to/large_file.zip",
    mode="chunk",
)

# Overwrite an existing file (uploads fail by default when the column already holds a file)
client.files.upload(
    "account", account_id, "new_Document", "/path/to/updated_contract.pdf",
    if_none_match=False,
)

Le mode paramètre contrôle la façon dont le Kit de développement logiciel (SDK) transfère le fichier.

Mode Comportement
"auto" (valeur par défaut) Sélectionne les "small" fichiers de moins de 128 Mo, ou "chunk" pour les fichiers de 128 Mo et plus.
"small" Envoie l’intégralité du fichier dans une seule requête. Lève une exception ValueError si le fichier dépasse 128 Mo.
"chunk" Diffuse le fichier dans des segments. Utiliser pour les fichiers volumineux ou forcer le transfert en bloc, quelle que soit la taille.

Note

Les restrictions de taille de fichier dataverse s’appliquent aux chargements de fichiers (128 Mo par fichier par défaut). Le chargement d’un fichier plus volumineux avec mode="small" déclenche un ValueError. Utilisez mode="chunk" ou la valeur par défaut mode="auto". Le client asynchrone expose la même files.upload méthode. Appelez-le avec await.

Opérations par lots

Permet client.batch d’envoyer plusieurs opérations dans une requête HTTP. L’espace de noms batch reflète client.records, client.tableset client.query.

# Build a batch request and add operations
batch = client.batch.new()
batch.records.create("account", {"name": "Contoso"})
batch.records.create("account", [{"name": "Fabrikam"}, {"name": "Woodgrove"}])
batch.records.update("account", account_id, {"telephone1": "555-0100"})
batch.records.delete("account", old_id)
batch.records.retrieve("account", account_id, select=["name"], expand=["primarycontactid"])  # single record with expand
batch.records.list(                                                # multi-record, single page
    "account",
    filter="statecode eq 0",
    select=["name"],
    orderby=["name asc"],
    top=50,
)

result = batch.execute()
for item in result.responses:
    if item.is_success:
        print(f"[OK] {item.status_code} entity_id={item.entity_id}")
    else:
        print(f"[ERR] {item.status_code}: {item.error_message}")

Ensemble de modifications transactionnelles

Toutes les opérations d’un jeu de modifications réussissent ou sont annulées ensemble.

batch = client.batch.new()
with batch.changeset() as cs:
    lead_ref = cs.records.create("lead", {"firstname": "Ada"})
    contact_ref = cs.records.create("contact", {"firstname": "Ada"})
    cs.records.create("account", {
        "name": "Babbage & Co.",
        "originatingleadid@odata.bind": lead_ref,
        "primarycontactid@odata.bind": contact_ref,
    })
result = batch.execute()
print(f"Created {len(result.entity_ids)} records atomically")

À l’intérieur d’un ensemble de modifications, cs.records.create() retourne une chaîne de référence content-ID telle que "$1". Utilisez-le dans le même jeu de modifications, sans attendre de réponse du serveur, soit comme valeur d’un champ @odata.bind (comme indiqué précédemment), soit comme argument record_id lors d’un appel ultérieur à cs.records.update() ou cs.records.delete(). Les opérations de lecture (retrieve, list) ne sont pas autorisées à l’intérieur d’un ensemble de modifications.

Métadonnées de table et requêtes SQL dans un lot

batch = client.batch.new()
batch.tables.create("new_Product", {"new_Price": "decimal", "new_InStock": "bool"})
batch.tables.add_columns("new_Product", {"new_Rating": "int"})
batch.tables.get("new_Product")
batch.query.sql("SELECT TOP 5 name FROM account")

result = batch.execute()

Continuer malgré l’erreur

Essayez toutes les opérations même en cas d’échec.

result = batch.execute(continue_on_error=True)
print(f"Succeeded: {len(result.succeeded)}, Failed: {len(result.failed)}")
for item in result.failed:
    print(f"[ERR] {item.status_code}: {item.error_message}")

Intégration de DataFrame

Importez directement des DataFrames de pandas dans un lot.

import pandas as pd

batch = client.batch.new()

# Create records from a DataFrame
df = pd.DataFrame([{"name": "Contoso"}, {"name": "Fabrikam"}])
batch.dataframe.create("account", df)

# Update records from a DataFrame
updates = pd.DataFrame([
    {"accountid": id1, "telephone1": "555-0100"},
    {"accountid": id2, "telephone1": "555-0200"},
])
batch.dataframe.update("account", updates, id_column="accountid")

# Delete records from a Series
batch.dataframe.delete("account", pd.Series([id1, id2]))

result = batch.execute()

Gérer les réponses par lots et les limites

batch.execute() retourne un BatchResult. Sa responses liste contient une entrée par opération HTTP dans l’ordre de soumission. Les listes succeeded et failed sont des sous-ensembles de responses. La has_errors propriété est True en cas d’échec d’une opération. La liste entity_ids collecte les GUID à partir de l’en-tête OData-EntityId des créations et mises à jour réussies d’un seul enregistrement.

Chaque entrée de réponse expose status_code, entity_id, is_success, data (corps analysé pour les opérations de récupération, de liste et de requête), error_message et error_code. Différentes opérations retournent différentes formes :

  • Création d’un seul enregistrement — statut 204, entity_id renseigné.
  • Création ou upsert en bloc : état 200, ID dans item.data["Ids"] (aucun OData-EntityId en-tête).
  • Requête ou liste SQL : état 200, lignes dans item.data["value"].
  • Suppression ou écriture de métadonnées — statut 204, aucune donnée dans le corps.

Le nombre maximal d’opérations HTTP par lot est de 1 000. Si vous dépassez cette limite, le client génère une ValidationError avant d’envoyer la demande. Certaines opérations donnent lieu à plusieurs requêtes : batch.tables.add_columns() et batch.tables.remove_columns() correspondent chacun à une requête par colonne, de sorte que chaque colonne produit sa propre entrée dans la réponse.

Pour obtenir un exemple de lot complet, consultez examples/advanced/batch.py.

Voir aussi