|
| 1 | +--- |
| 2 | +name: powerplatform-dataverseclient-python |
| 3 | +description: Guidance for using the PowerPlatform Dataverse Client Python SDK. Use when calling the SDK like creating CRUD operations, SQL queries, table metadata management, and upload files. |
| 4 | +--- |
| 5 | + |
| 6 | +# PowerPlatform Dataverse SDK Guide |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Use the PowerPlatform Dataverse Client Python SDK to interact with Microsoft Dataverse. |
| 11 | + |
| 12 | +## Key Concepts |
| 13 | + |
| 14 | +### Schema Names vs Display Names |
| 15 | +- Standard tables: lowercase (e.g., `"account"`, `"contact"`) |
| 16 | +- Custom tables: include customization prefix (e.g., `"new_Product"`, `"cr123_Invoice"`) |
| 17 | +- Custom columns: include customization prefix (e.g., `"new_Price"`, `"cr123_Status"`) |
| 18 | +- ALWAYS use **schema names** (logical names), NOT display names |
| 19 | + |
| 20 | +### Bulk Operations |
| 21 | +The SDK supports Dataverse's native bulk operations: Pass lists to `create()`, `update()` for automatic bulk processing, for `delete()`, set `use_bulk_delete` when passing lists to use bulk operation |
| 22 | + |
| 23 | +### Paging |
| 24 | +- Control page size with `page_size` parameter |
| 25 | +- Use `top` parameter to limit total records returned |
| 26 | + |
| 27 | +## Common Operations |
| 28 | + |
| 29 | +### Import |
| 30 | +```python |
| 31 | +from azure.identity import ( |
| 32 | + InteractiveBrowserCredential, |
| 33 | + ClientSecretCredential, |
| 34 | + ClientCertificateCredential, |
| 35 | + AzureCliCredential |
| 36 | +) |
| 37 | +from PowerPlatform.Dataverse.client import DataverseClient |
| 38 | +``` |
| 39 | + |
| 40 | +### Client Initialization |
| 41 | +```python |
| 42 | +# Development options |
| 43 | +credential = InteractiveBrowserCredential() |
| 44 | +credential = AzureCliCredential() |
| 45 | + |
| 46 | +# Production options |
| 47 | +credential = ClientSecretCredential(tenant_id, client_id, client_secret) |
| 48 | +credential = ClientCertificateCredential(tenant_id, client_id, cert_path) |
| 49 | + |
| 50 | +# Create client (no trailing slash on URL!) |
| 51 | +client = DataverseClient("https://yourorg.crm.dynamics.com", credential) |
| 52 | +``` |
| 53 | + |
| 54 | +### CRUD Operations |
| 55 | + |
| 56 | +#### Create Records |
| 57 | +```python |
| 58 | +# Single record |
| 59 | +account_ids = client.create("account", {"name": "Contoso Ltd", "telephone1": "555-0100"}) |
| 60 | +account_id = account_ids[0] |
| 61 | + |
| 62 | +# Bulk create (uses CreateMultiple API automatically) |
| 63 | +contacts = [ |
| 64 | + {"firstname": "John", "lastname": "Doe"}, |
| 65 | + {"firstname": "Jane", "lastname": "Smith"} |
| 66 | +] |
| 67 | +contact_ids = client.create("contact", contacts) |
| 68 | +``` |
| 69 | + |
| 70 | +#### Read Records |
| 71 | +```python |
| 72 | +# Get single record by ID |
| 73 | +account = client.get("account", account_id, select=["name", "telephone1"]) |
| 74 | + |
| 75 | +# Query with filter |
| 76 | +pages = client.get( |
| 77 | + "account", |
| 78 | + select=["accountid", "name"], # select is case-insensitive (automatically lowercased) |
| 79 | + filter="statecode eq 0", # filter must use lowercase logical names (not transformed) |
| 80 | + top=100 |
| 81 | +) |
| 82 | +for page in pages: |
| 83 | + for record in page: |
| 84 | + print(record["name"]) |
| 85 | + |
| 86 | +# Query with navigation property expansion (case-sensitive!) |
| 87 | +pages = client.get( |
| 88 | + "account", |
| 89 | + select=["name"], |
| 90 | + expand=["primarycontactid"], # Navigation properties are case-sensitive! |
| 91 | + filter="statecode eq 0" # Column names must be lowercase logical names |
| 92 | +) |
| 93 | +for page in pages: |
| 94 | + for account in page: |
| 95 | + contact = account.get("primarycontactid", {}) |
| 96 | + print(f"{account['name']} - {contact.get('fullname', 'N/A')}") |
| 97 | +``` |
| 98 | + |
| 99 | +#### Update Records |
| 100 | +```python |
| 101 | +# Single update |
| 102 | +client.update("account", account_id, {"telephone1": "555-0200"}) |
| 103 | + |
| 104 | +# Bulk update (broadcast same change to multiple records) |
| 105 | +client.update("account", [id1, id2, id3], {"industry": "Technology"}) |
| 106 | +``` |
| 107 | + |
| 108 | +#### Delete Records |
| 109 | +```python |
| 110 | +# Single delete |
| 111 | +client.delete("account", account_id) |
| 112 | + |
| 113 | +# Bulk delete (uses BulkDelete API) |
| 114 | +client.delete("account", [id1, id2, id3], use_bulk_delete=True) |
| 115 | +``` |
| 116 | + |
| 117 | +### SQL Queries |
| 118 | + |
| 119 | +SQL queries are **read-only** and support limited SQL syntax. A single SELECT statement with optional WHERE, TOP (integer literal), ORDER BY (column names only), and a simple table alias after FROM is supported. But JOIN and subqueries may not be. Refer to the Dataverse documentation for the current feature set. |
| 120 | + |
| 121 | +```python |
| 122 | +# Basic SQL query |
| 123 | +results = client.query_sql( |
| 124 | + "SELECT TOP 10 accountid, name FROM account WHERE statecode = 0" |
| 125 | +) |
| 126 | +for record in results: |
| 127 | + print(record["name"]) |
| 128 | +``` |
| 129 | + |
| 130 | +### Table Management |
| 131 | + |
| 132 | +#### Create Custom Tables |
| 133 | +```python |
| 134 | +# Create table with columns (include customization prefix!) |
| 135 | +table_info = client.create_table( |
| 136 | + table_schema_name="new_Product", |
| 137 | + columns={ |
| 138 | + "new_Code": "string", |
| 139 | + "new_Price": "decimal", |
| 140 | + "new_Active": "bool", |
| 141 | + "new_Quantity": "int" |
| 142 | + } |
| 143 | +) |
| 144 | + |
| 145 | +# With solution assignment and custom primary column |
| 146 | +table_info = client.create_table( |
| 147 | + table_schema_name="new_Product", |
| 148 | + columns={"new_Code": "string", "new_Price": "decimal"}, |
| 149 | + solution_unique_name="MyPublisher", |
| 150 | + primary_column_schema_name="new_ProductCode" |
| 151 | +) |
| 152 | +``` |
| 153 | + |
| 154 | +#### Supported Column Types |
| 155 | +Types on the same line map to the same exact format under the hood |
| 156 | +- `"string"` or `"text"` - Single line of text |
| 157 | +- `"int"` or `"integer"` - Whole number |
| 158 | +- `"decimal"` or `"money"` - Decimal number |
| 159 | +- `"float"` or `"double"` - Floating point number |
| 160 | +- `"bool"` or `"boolean"` - Yes/No |
| 161 | +- `"datetime"` or `"date"` - Date |
| 162 | +- Enum subclass - Local option set (picklist) |
| 163 | + |
| 164 | +#### Manage Columns |
| 165 | +```python |
| 166 | +# Add columns to existing table (must include customization prefix!) |
| 167 | +client.create_columns("new_Product", { |
| 168 | + "new_Category": "string", |
| 169 | + "new_InStock": "bool" |
| 170 | +}) |
| 171 | + |
| 172 | +# Remove columns |
| 173 | +client.delete_columns("new_Product", ["new_Category"]) |
| 174 | +``` |
| 175 | + |
| 176 | +#### Inspect Tables |
| 177 | +```python |
| 178 | +# Get single table information |
| 179 | +table_info = client.get_table_info("new_Product") |
| 180 | +print(f"Logical name: {table_info['table_logical_name']}") |
| 181 | +print(f"Entity set: {table_info['entity_set_name']}") |
| 182 | + |
| 183 | +# List all tables |
| 184 | +tables = client.list_tables() |
| 185 | +for table in tables: |
| 186 | + print(table) |
| 187 | +``` |
| 188 | + |
| 189 | +#### Delete Tables |
| 190 | +```python |
| 191 | +# Delete custom table |
| 192 | +client.delete_table("new_Product") |
| 193 | +``` |
| 194 | + |
| 195 | +### File Operations |
| 196 | + |
| 197 | +```python |
| 198 | +# Upload file to a file column |
| 199 | +client.upload_file( |
| 200 | + table_schema_name="account", |
| 201 | + record_id=account_id, |
| 202 | + file_name_attribute="new_document", |
| 203 | + path="/path/to/document.pdf" |
| 204 | +) |
| 205 | +``` |
| 206 | + |
| 207 | +## Error Handling |
| 208 | + |
| 209 | +The SDK provides structured exceptions with detailed error information: |
| 210 | + |
| 211 | +```python |
| 212 | +from PowerPlatform.Dataverse.core.errors import ( |
| 213 | + DataverseError, |
| 214 | + HttpError, |
| 215 | + ValidationError, |
| 216 | + MetadataError, |
| 217 | + SQLParseError |
| 218 | +) |
| 219 | +from PowerPlatform.Dataverse.client import DataverseClient |
| 220 | + |
| 221 | +try: |
| 222 | + client.get("account", "invalid-id") |
| 223 | +except HttpError as e: |
| 224 | + print(f"HTTP {e.status_code}: {e.message}") |
| 225 | + print(f"Error code: {e.code}") |
| 226 | + print(f"Subcode: {e.subcode}") |
| 227 | + if e.is_transient: |
| 228 | + print("This error may be retryable") |
| 229 | +except ValidationError as e: |
| 230 | + print(f"Validation error: {e.message}") |
| 231 | +``` |
| 232 | + |
| 233 | +### Common Error Patterns |
| 234 | + |
| 235 | +**Authentication failures:** |
| 236 | +- Check environment URL format (no trailing slash) |
| 237 | +- Verify credentials have Dataverse permissions |
| 238 | +- Ensure app registration is properly configured |
| 239 | + |
| 240 | +**404 Not Found:** |
| 241 | +- Verify table schema name is correct (lowercase for standard tables) |
| 242 | +- Check record ID exists |
| 243 | +- Ensure using schema names, not display names |
| 244 | +- Cache issue could happen, so retry might help, especially for metadata creation |
| 245 | + |
| 246 | +**400 Bad Request:** |
| 247 | +- Check filter/expand parameters use correct case |
| 248 | +- Verify column names exist and are spelled correctly |
| 249 | +- Ensure custom columns include customization prefix |
| 250 | + |
| 251 | +## Best Practices |
| 252 | + |
| 253 | +### Performance Optimization |
| 254 | + |
| 255 | +1. **Use bulk operations** - Pass lists to create/update/delete for automatic optimization |
| 256 | +2. **Specify select fields** - Limit returned columns to reduce payload size |
| 257 | +3. **Control page size** - Use `top` and `page_size` parameters appropriately |
| 258 | +4. **Reuse client instances** - Don't create new clients for each operation |
| 259 | +5. **Use production credentials** - ClientSecretCredential or ClientCertificateCredential for unattended operations |
| 260 | +6. **Error handling** - Implement retry logic for transient errors (`e.is_transient`) |
| 261 | +7. **Always include customization prefix** for custom tables/columns |
| 262 | +8. **Use lowercase** - Generally using lower cased input won't go wrong, exception would be custom tables/columns naming |
| 263 | +9. **Test in non-production environments** first |
| 264 | + |
| 265 | +## Additional Resources |
| 266 | + |
| 267 | +Load these resources as needed during development: |
| 268 | + |
| 269 | +- [API Reference](https://learn.microsoft.com/python/api/dataverse-sdk-docs-python/dataverse-overview) |
| 270 | +- [Product Documentation](https://learn.microsoft.com/power-apps/developer/data-platform/sdk-python/) |
| 271 | +- [Dataverse Web API](https://learn.microsoft.com/power-apps/developer/data-platform/webapi/) |
| 272 | +- [Azure Identity](https://learn.microsoft.com/python/api/overview/azure/identity-readme) |
| 273 | + |
| 274 | +## Key Reminders |
| 275 | + |
| 276 | +1. **Schema names are required** - Never use display names |
| 277 | +2. **Custom tables need prefixes** - Include customization prefix (e.g., "new_") |
| 278 | +3. **Filter is case-sensitive** - Use lowercase logical names |
| 279 | +4. **Bulk operations are encouraged** - Pass lists for optimization |
| 280 | +5. **No trailing slashes in URLs** - Format: `https://org.crm.dynamics.com` |
| 281 | +6. **Structured errors** - Check `is_transient` for retry logic |
0 commit comments