You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
6.**Internal vs public naming** - Modules, files, and functions not meant to be part of the public API must use a `_` prefix (e.g., `_odata.py`, `_relationships.py`). Files without the prefix (e.g., `constants.py`, `metadata.py`) are public and importable by SDK consumers
22
22
23
+
### Dataverse Property Naming Rules
24
+
25
+
Dataverse uses two different naming conventions for properties. Getting this wrong causes 400 errors that are hard to debug.
26
+
27
+
| Property type | Name convention | Example | When used |
Navigation property names are case-sensitive and must match the entity's `$metadata`. Using the logical name instead of the navigation property name results in 400 Bad Request errors.
33
+
34
+
**Critical rule:** The OData parser validates `@odata.bind` property names **case-sensitively** against declared navigation properties. Lowercasing `new_CustomerId@odata.bind` to `new_customerid@odata.bind` causes: `ODataException: An undeclared property 'new_customerid' which only has property annotations...`
35
+
36
+
**SDK implementation:**
37
+
38
+
-`_lowercase_keys()` lowercases all keys EXCEPT those containing `@odata.` (preserves navigation property casing in `@odata.bind` keys)
39
+
-`_lowercase_list()` lowercases `$select` and `$orderby` params (structural properties)
40
+
-`$expand` params are passed as-is (navigation properties, PascalCase)
41
+
-`_convert_labels_to_ints()` skips `@odata.` keys entirely (they are annotations, not attributes)
42
+
43
+
**When adding new code that processes record dicts or builds query parameters:**
44
+
45
+
- Always use `_lowercase_keys()` for record payloads. Never manually call `.lower()` on all keys
46
+
- Never lowercase `$expand` values or `@odata.bind` key prefixes
47
+
- If iterating record keys, skip keys containing `@odata.` when doing attribute-level operations
48
+
23
49
### Code Style
24
50
25
51
6.**No emojis** - Do not use emoji in code, comments, or output
Copy file name to clipboardExpand all lines: .claude/skills/dataverse-sdk-use/SKILL.md
+56-2Lines changed: 56 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,6 +30,9 @@ The SDK supports Dataverse's native bulk operations: Pass lists to `create()`, `
30
30
- Control page size with `page_size` parameter
31
31
- Use `top` parameter to limit total records returned
32
32
33
+
### DataFrame Support
34
+
- DataFrame operations are accessed via the `client.dataframe` namespace: `client.dataframe.get()`, `client.dataframe.create()`, `client.dataframe.update()`, `client.dataframe.delete()`
35
+
33
36
## Common Operations
34
37
35
38
### Import
@@ -105,6 +108,20 @@ for page in client.records.get(
Creates or updates records identified by alternate keys. Single item → PATCH; multiple items →`UpsertMultiple` bulk action.
135
+
Creates or updates records identified by alternate keys. Single item -> PATCH; multiple items ->`UpsertMultiple` bulk action.
119
136
> **Prerequisite**: The table must have an alternate key configured in Dataverse for the columns used in `alternate_key`. Without it, Dataverse will reject the request with a 400 error.
120
137
```python
121
138
from PowerPlatform.Dataverse.models.upsert import UpsertItem
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.
@@ -359,6 +412,7 @@ except ValidationError as e:
359
412
- Check filter/expand parameters use correct case
360
413
- Verify column names exist and are spelled correctly
361
414
- Ensure custom columns include customization prefix
415
+
- For `@odata.bind` errors ("undeclared property"): the navigation property name before `@odata.bind` is case-sensitive and must match the entity's `$metadata` exactly (e.g., `new_CustomerId@odata.bind` for custom lookups, `parentaccountid@odata.bind` for system lookups). The SDK preserves `@odata.bind` key casing.
362
416
363
417
## Best Practices
364
418
@@ -371,7 +425,7 @@ except ValidationError as e:
371
425
5.**Use production credentials** - ClientSecretCredential or CertificateCredential for unattended operations
372
426
6.**Error handling** - Implement retry logic for transient errors (`e.is_transient`)
373
427
7.**Always include customization prefix** for custom tables/columns
374
-
8.**Use lowercase** - Generally using lowercase input won't go wrong, except for custom table/column naming
428
+
8.**Use lowercase for column names, match `$metadata` for navigation properties** - Column names in `$select`/`$filter`/record payloads use lowercase LogicalNames. Navigation properties in `$expand` and `@odata.bind` keys are case-sensitive and must match the entity's `$metadata` (PascalCase for custom lookups like `new_CustomerId`, lowercase for system lookups like `parentaccountid`)
375
429
9.**Test in non-production environments** first
376
430
10.**Use named constants** - Import cascade behavior constants from `PowerPlatform.Dataverse.common.constants`
Copy file name to clipboardExpand all lines: CHANGELOG.md
+13Lines changed: 13 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
8
+
## [0.1.0b6] - 2026-03-12
9
+
10
+
### Added
11
+
- Context manager support: `with DataverseClient(...) as client:` for automatic resource cleanup, HTTP connection pooling, and `close()` for explicit lifecycle management (#117)
12
+
- Typed return models `Record`, `TableInfo`, and `ColumnInfo` for record and table metadata operations, replacing raw `Dict[str, Any]` returns with full backward compatibility (`result["key"]` still works) (#115)
13
+
- Alternate key management: `client.tables.create_alternate_key()`, `client.tables.get_alternate_keys()`, `client.tables.delete_alternate_key()` with typed `AlternateKeyInfo` model (#126)
14
+
15
+
### Fixed
16
+
-`@odata.bind` lookup bindings now preserve navigation property casing (e.g., `new_CustomerId@odata.bind`), fixing `400 Bad Request` errors on create/update/upsert with lookup fields (#137)
17
+
- Reduced unnecessary HTTP round-trips on create/update/upsert when records contain `@odata.bind` keys (#137)
18
+
- Single-record `get()` now lowercases `$select` column names consistently with multi-record queries (#137)
19
+
8
20
## [0.1.0b5] - 2026-02-27
9
21
10
22
### Fixed
@@ -70,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
70
82
- Comprehensive error handling with specific exception types (`DataverseError`, `AuthenticationError`, etc.) (#22, #24)
71
83
- HTTP retry logic with exponential backoff for resilient operations (#72)
The **QueryBuilder** is the recommended way to query records. It provides a fluent, type-safe interface that generates correct OData queries automatically — no need to remember OData filter syntax.
@@ -252,6 +290,107 @@ for record in (client.query.builder("account")
252
290
253
291
The QueryBuilder handles value formatting, column name casing, and OData syntax automatically. All filter methods are discoverable via IDE autocomplete:
254
292
293
+
```python
294
+
# Get results as a pandas DataFrame (consolidates all pages)
**Record count** -- include `$count=true` in the request:
366
+
367
+
```python
368
+
# Request count alongside results
369
+
results = (client.query.builder("account")
370
+
.filter_eq("statecode", 0)
371
+
.count()
372
+
.execute())
373
+
```
374
+
375
+
**SQL queries** provide an alternative read-only query syntax:
376
+
377
+
The **QueryBuilder** is the recommended way to query records. It provides a fluent, type-safe interface that generates correct OData queries automatically — no need to remember OData filter syntax.
378
+
379
+
```python
380
+
# Fluent query builder (recommended)
381
+
for record in (client.query.builder("account")
382
+
.select("name", "revenue")
383
+
.filter_eq("statecode", 0)
384
+
.filter_gt("revenue", 1000000)
385
+
.order_by("revenue", descending=True)
386
+
.top(100)
387
+
.page_size(50)
388
+
.execute()):
389
+
print(f"{record['name']}: {record['revenue']}")
390
+
```
391
+
392
+
The QueryBuilder handles value formatting, column name casing, and OData syntax automatically. All filter methods are discoverable via IDE autocomplete:
0 commit comments