dataverse-sdk-dev
Development guidance for contributing to the PowerPlatform Dataverse Client Python SDK repository. Use when working on SDK development tasks like adding…
npx skills add https://github.com/microsoft/powerplatform-dataverseclient-python --skill dataverse-sdk-devDataverse 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
- 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). Theclient.pyfile 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) - Every public method needs README example - Public API methods must have examples in README.md
- Reuse existing APIs - Always check if an existing method can be used before making direct Web API calls
- Update documentation when adding features - Keep README and SKILL files (both copies) in sync
- Consider backwards compatibility - Avoid breaking changes
- 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 - Async client - The SDK ships a full async client (
AsyncDataverseClient) undersrc/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 indata/_odata_base.py— inherited by both_ODataClientand_AsyncODataClient— so it only needs to be written once; HTTP-calling code goes indata/_odata.py(sync) oraio/data/_async_odata.py(async). Async tests live intests/unit/aio/and async examples inexamples/aio/. Theaiohttpdependency is an optional extra (pip install "PowerPlatform-Dataverse-Client[async]") — do not move it into the requireddependencieslist inpyproject.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.bindkeys)_lowercase_list()lowercases$selectand$orderbyparams (structural properties)$expandparams 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
$expandvalues or@odata.bindkey prefixes - If iterating record keys, skip keys containing
@odata.when doing attribute-level operations
Code Style
- No emojis - Do not use emoji in code, comments, or output
- Standardize output format - Use
[INFO],[WARN],[ERR],[OK]prefixes for console output - No noqa comments - Do not add
# noqa: BLE001or similar linter suppression comments - Document public APIs - Add Sphinx-style docstrings with examples for public methods
- Define all in module files - Each module declares its own exports via
__all__(e.g.,errors.pydefines__all__ = ["HttpError", ...]). Package__init__.pyfiles should not re-export or redefine another module's__all__; they use__all__ = []to indicate no star-import exports. - Run black before committing - Always run
python -m black <changed files>before committing. CI will reject unformatted code. Config is inpyproject.tomlunder[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 brokenxref: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 wordsof, mapping, tobecome brokenxref:` links.
Correct examples:
: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):
: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`