# Dataverse Sdk Dev

> Development guidance for contributing to the PowerPlatform Dataverse Client Python SDK repository. Use when working on SDK development tasks like adding features, fixing bugs, or writing tests.

- Skill: `microsoft/dataverse-sdk-dev` (Agent Skill)
- Install (CLI): `npx skillmds@latest add microsoft/dataverse-sdk-dev`
- Raw SKILL.md: https://api.skillmd.com/api/skills/microsoft/dataverse-sdk-dev/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Microsoft (https://skillmd.com/u/microsoft)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/microsoft/dataverse-sdk-dev

---


# Dataverse SDK Development Guide

## Overview

This skill provides guidance for developers working on the PowerPlatform Dataverse Client Python SDK repository itself (not using the SDK).

## Best Practices

### API Design

1. **Public methods in operation namespaces** - New public methods go in the appropriate namespace module under `src/PowerPlatform/Dataverse/operations/` (`records.py`, `query.py`, `tables.py`, `batch.py`). The `client.py` file exposes these via namespace properties (`client.records`, `client.query`, `client.tables`, `client.batch`). Public types and constants live in their own modules (e.g., `models/metadata.py`, `models/batch.py`, `common/constants.py`)
2. **Every public method needs README example** - Public API methods must have examples in README.md
3. **Reuse existing APIs** - Always check if an existing method can be used before making direct Web API calls
4. **Update documentation** when adding features - Keep README and SKILL files (both copies) in sync
5. **Consider backwards compatibility** - Avoid breaking changes
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
7. **Async client** - The SDK ships a full async client (`AsyncDataverseClient`) under `src/PowerPlatform/Dataverse/aio/`. When adding a feature to the sync client, add it to the async client too. The async operation namespaces mirror the sync ones: `aio/operations/async_records.py`, `async_query.py`, `async_tables.py`, `async_batch.py`, `async_files.py`. Pure logic (payload builders, URL construction) goes in `data/_odata_base.py` — inherited by both `_ODataClient` and `_AsyncODataClient` — so it only needs to be written once; HTTP-calling code goes in `data/_odata.py` (sync) or `aio/data/_async_odata.py` (async). Async tests live in `tests/unit/aio/` and async examples in `examples/aio/`. The `aiohttp` dependency is an optional extra (`pip install "PowerPlatform-Dataverse-Client[async]"`) — do not move it into the required `dependencies` list in `pyproject.toml`.

### Dataverse Property Naming Rules

Dataverse uses two different naming conventions for properties. Getting this wrong causes 400 errors that are hard to debug.

| Property type | Name convention | Example | When used |
|---|---|---|---|
| **Structural** (columns) | LogicalName (always lowercase) | `new_name`, `new_priority` | `$select`, `$filter`, `$orderby`, record payload keys |
| **Navigation** (relationships / lookups) | Navigation Property Name (usually SchemaName, PascalCase, case-sensitive) | `new_CustomerId`, `new_AgentId` | `$expand`, `@odata.bind` annotation keys |

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.

**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...`

**SDK implementation:**

- `_lowercase_keys()` lowercases all keys EXCEPT those containing `@odata.` (preserves navigation property casing in `@odata.bind` keys)
- `_lowercase_list()` lowercases `$select` and `$orderby` params (structural properties)
- `$expand` params are passed as-is (navigation properties, PascalCase)
- `_convert_labels_to_ints()` skips `@odata.` keys entirely (they are annotations, not attributes)

**When adding new code that processes record dicts or builds query parameters:**

- Always use `_lowercase_keys()` for record payloads. Never manually call `.lower()` on all keys
- Never lowercase `$expand` values or `@odata.bind` key prefixes
- If iterating record keys, skip keys containing `@odata.` when doing attribute-level operations

### Code Style

6. **No emojis** - Do not use emoji in code, comments, or output
7. **Standardize output format** - Use `[INFO]`, `[WARN]`, `[ERR]`, `[OK]` prefixes for console output
8. **No noqa comments** - Do not add `# noqa: BLE001` or similar linter suppression comments
9. **Document public APIs** - Add Sphinx-style docstrings with examples for public methods
10. **Define __all__ in module files** - Each module declares its own exports via `__all__` (e.g., `errors.py` defines `__all__ = ["HttpError", ...]`). Package `__init__.py` files should not re-export or redefine another module's `__all__`; they use `__all__ = []` to indicate no star-import exports.
11. **Run black before committing** - Always run `python -m black <changed files>` before committing. CI will reject unformatted code. Config is in `pyproject.toml` under `[tool.black]`.

### Docstring Type Annotations (Microsoft Learn Compatibility)

This SDK's API reference is published on Microsoft Learn. The Learn doc pipeline parses `:type:` and `:rtype:` directives differently from standard Sphinx -- every word between `:class:` references is treated as a separate cross-reference (`<xref:word>`). Using Sphinx-style `:class:\`list\` of :class:\`str\`` produces broken `<xref:of>` links on Learn.

**Rules for `:type:` and `:rtype:` directives:**

- Use Python bracket notation for generic types: `list[str]`, `dict[str, typing.Any]`, `list[dict]`
- Use `or` (without `:class:`) for union types: `str or None`, `dict or list[dict]`
- Use bracket nesting for complex types: `collections.abc.Iterable[list[dict]]`
- Use `~` prefix for SDK types to show short name: `list[~PowerPlatform.Dataverse.models.record.Record]`
- `:class:` is fine for single standalone types: `:class:\`str\``, `:class:\`bool\``

**Never** use `:class:\`X\` of :class:\`Y\`` or `:class:\`X\` mapping :class:\`Y\` to :class:\`Z\`` -- the words `of`, `mapping`, `to` become broken `<xref:>` links.

**Correct examples:**

```rst
:type data: dict or list[dict]
:rtype: list[str]
:rtype: collections.abc.Iterable[list[~PowerPlatform.Dataverse.models.record.Record]]
:type select: list[str] or None
:type columns: dict[str, typing.Any]
```

**Wrong examples (NEVER use):**

```rst
:type data: :class:`dict` or :class:`list` of :class:`dict`
:rtype: :class:`list` of :class:`str`
:type columns: :class:`dict` mapping :class:`str` to :class:`typing.Any`
```

