Skip to content

Commit 48bae0d

Browse files
authored
Merge branch 'main' into users/sagebree/codmod_fixes
2 parents 6e213c9 + 5c85552 commit 48bae0d

78 files changed

Lines changed: 20285 additions & 82 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.azdo/ci-pr.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ extends:
4343
- script: |
4444
python -m pip install --upgrade pip
4545
python -m pip install flake8 black build diff-cover
46-
python -m pip install -e .[dev]
46+
python -m pip install -e .[dev,async]
4747
displayName: 'Install dependencies'
4848
4949
- script: |

.claude/skills/dataverse-sdk-dev/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This skill provides guidance for developers working on the PowerPlatform Dataver
1919
4. **Update documentation** when adding features - Keep README and SKILL files (both copies) in sync
2020
5. **Consider backwards compatibility** - Avoid breaking changes
2121
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+
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`.
2223

2324
### Dataverse Property Naming Rules
2425

.claude/skills/dataverse-sdk-use/SKILL.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,115 @@ except ValidationError as e:
588588
10. **Test in non-production environments** first
589589
11. **Use named constants** - Import cascade behavior constants from `PowerPlatform.Dataverse.common.constants`
590590

591+
## Async Client
592+
593+
The SDK ships a full async client, `AsyncDataverseClient`, under `PowerPlatform.Dataverse.aio`. Requires the `[async]` extra: `pip install "PowerPlatform-Dataverse-Client[async]"`.
594+
595+
### Import
596+
```python
597+
from azure.identity.aio import DefaultAzureCredential
598+
from PowerPlatform.Dataverse.aio import AsyncDataverseClient
599+
```
600+
601+
### Client Initialization
602+
```python
603+
# Context manager (recommended -- closes session and clears caches automatically)
604+
async with AsyncDataverseClient("https://yourorg.crm.dynamics.com", credential) as client:
605+
... # all operations here
606+
607+
# Standalone (call aclose() in a finally block)
608+
client = AsyncDataverseClient("https://yourorg.crm.dynamics.com", credential)
609+
try:
610+
...
611+
finally:
612+
await client.aclose()
613+
```
614+
615+
### CRUD Operations
616+
Every sync method has an async equivalent -- add `await`:
617+
```python
618+
# Create
619+
account_id = await client.records.create("account", {"name": "Contoso Ltd"})
620+
621+
# Read
622+
account = await client.records.retrieve("account", account_id, select=["name", "telephone1"])
623+
624+
# Update
625+
await client.records.update("account", account_id, {"telephone1": "555-0200"})
626+
627+
# Delete
628+
await client.records.delete("account", account_id)
629+
630+
# Bulk create
631+
ids = await client.records.create("account", [{"name": "A"}, {"name": "B"}])
632+
```
633+
634+
### Query Builder
635+
```python
636+
from PowerPlatform.Dataverse.models.filters import col
637+
638+
# Collect all results
639+
result = await (
640+
client.query.builder("account")
641+
.select("name", "telephone1")
642+
.where(col("statecode") == 0)
643+
.top(10)
644+
.execute()
645+
)
646+
for record in result:
647+
print(record["name"])
648+
649+
# Lazy page iteration (memory-efficient)
650+
async for page in (
651+
client.query.builder("account")
652+
.select("name")
653+
.page_size(500)
654+
.execute_pages()
655+
):
656+
for record in page:
657+
print(record["name"])
658+
659+
# SQL query
660+
rows = await client.query.sql("SELECT TOP 5 name FROM account")
661+
662+
# FetchXML
663+
xml = '<fetch top="5"><entity name="account"><attribute name="name"/></entity></fetch>'
664+
rows = await client.query.fetchxml(xml).execute()
665+
```
666+
667+
### Batch and Changesets
668+
```python
669+
# Plain batch
670+
batch = client.batch.new()
671+
batch.records.create("account", {"name": "Alpha"})
672+
result = await batch.execute()
673+
674+
# Atomic changeset
675+
batch = client.batch.new()
676+
async with batch.changeset() as cs:
677+
ref = cs.records.create("contact", {"firstname": "Alice"})
678+
cs.records.update("account", account_id, {"primarycontactid@odata.bind": ref})
679+
result = await batch.execute()
680+
```
681+
682+
### DataFrame Operations
683+
```python
684+
import pandas as pd
685+
686+
# Query to DataFrame
687+
result = await (
688+
client.query.builder("account")
689+
.select("name", "telephone1")
690+
.where(col("statecode") == 0)
691+
.execute()
692+
)
693+
df = result.to_dataframe()
694+
695+
# Create from DataFrame
696+
new_accounts = pd.DataFrame([{"name": "Contoso"}, {"name": "Fabrikam"}])
697+
ids = await client.dataframe.create("account", new_accounts)
698+
```
699+
591700
## Additional Resources
592701

593702
Load these resources as needed during development:

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
run: |
3131
python -m pip install --upgrade pip
3232
python -m pip install flake8 black build diff-cover
33-
python -m pip install -e .[dev]
33+
python -m pip install -e .[dev,async]
3434
3535
- name: Check format with black
3636
run: |

README.md

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A Python client library for Microsoft Dataverse that provides a unified interfac
1616
- [Key features](#key-features)
1717
- [Getting started](#getting-started)
1818
- [Prerequisites](#prerequisites)
19-
- [Install the package](#install-the-package)
19+
- [Install the package](#install-the-package)
2020
- [Authenticate the client](#authenticate-the-client)
2121
- [Key concepts](#key-concepts)
2222
- [Examples](#examples)
@@ -30,6 +30,7 @@ A Python client library for Microsoft Dataverse that provides a unified interfac
3030
- [Relationship management](#relationship-management)
3131
- [File operations](#file-operations)
3232
- [Batch operations](#batch-operations)
33+
- [Async client](#async-client)
3334
- [Next steps](#next-steps)
3435
- [Troubleshooting](#troubleshooting)
3536
- [Contributing](#contributing)
@@ -53,7 +54,7 @@ A Python client library for Microsoft Dataverse that provides a unified interfac
5354

5455
### Prerequisites
5556

56-
- **Python 3.10+** (3.10, 3.11, 3.12, 3.13 supported)
57+
- **Python 3.10+** (3.10, 3.11, 3.12, 3.13 supported)
5758
- **Microsoft Dataverse environment** with appropriate permissions
5859
- **OAuth authentication configured** for your application
5960

@@ -92,7 +93,7 @@ The client requires any Azure Identity `TokenCredential` implementation for OAut
9293

9394
```python
9495
from azure.identity import (
95-
InteractiveBrowserCredential,
96+
InteractiveBrowserCredential,
9697
ClientSecretCredential,
9798
CertificateCredential,
9899
AzureCliCredential
@@ -103,7 +104,7 @@ from PowerPlatform.Dataverse.client import DataverseClient
103104
credential = InteractiveBrowserCredential() # Browser authentication
104105
# credential = AzureCliCredential() # If logged in via 'az login'
105106

106-
# Production options
107+
# Production options
107108
# credential = ClientSecretCredential(tenant_id, client_id, client_secret)
108109
# credential = CertificateCredential(tenant_id, client_id, cert_path)
109110

@@ -209,7 +210,7 @@ a PATCH request; multiple items use the `UpsertMultiple` bulk action.
209210
> upsert requests will be rejected by Dataverse with a 400 error.
210211
211212
```python
212-
from PowerPlatform.Dataverse.models.upsert import UpsertItem
213+
from PowerPlatform.Dataverse.models import UpsertItem
213214

214215
# Upsert a single record
215216
client.records.upsert("account", [
@@ -346,7 +347,7 @@ query = (client.query.builder("contact")
346347
For complex logic (OR, NOT, grouping), compose expressions with `&`, `|`, `~`:
347348

348349
```python
349-
from PowerPlatform.Dataverse.models.filters import col
350+
from PowerPlatform.Dataverse.models import col
350351

351352
# OR conditions: (statecode = 0 OR statecode = 1) AND revenue > 100k
352353
for record in (client.query.builder("account")
@@ -397,7 +398,7 @@ if record:
397398
**Nested expand with options** -- expand navigation properties with `$select`, `$filter`, `$orderby`, and `$top`:
398399

399400
```python
400-
from PowerPlatform.Dataverse.models.query_builder import ExpandOption
401+
from PowerPlatform.Dataverse.models import ExpandOption
401402

402403
# Expand related tasks with filtering and sorting
403404
for record in (client.query.builder("account")
@@ -614,12 +615,14 @@ client.tables.delete("new_Product")
614615
Create relationships between tables using the relationship API. For a complete working example, see [examples/advanced/relationships.py](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/blob/main/examples/advanced/relationships.py).
615616

616617
```python
617-
from PowerPlatform.Dataverse.models.relationship import (
618+
from PowerPlatform.Dataverse.models import (
619+
CascadeConfiguration,
620+
Label,
621+
LocalizedLabel,
618622
LookupAttributeMetadata,
619-
OneToManyRelationshipMetadata,
620623
ManyToManyRelationshipMetadata,
624+
OneToManyRelationshipMetadata,
621625
)
622-
from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel
623626

624627
# Create a one-to-many relationship: Department (1) -> Employee (N)
625628
# This adds a "Department" lookup field to the Employee table
@@ -783,6 +786,99 @@ result = batch.execute()
783786

784787
For a complete example see [examples/advanced/batch.py](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/blob/main/examples/advanced/batch.py).
785788

789+
## Async client
790+
791+
The SDK ships a full async client, `AsyncDataverseClient`, for use in async applications. It mirrors every operation of the sync client — the same namespaces (`records`, `query`, `tables`, `files`, `batch`), the same method signatures, and the same return types.
792+
793+
### Install
794+
795+
The async client requires `aiohttp`, which is an optional extra:
796+
797+
```bash
798+
pip install "PowerPlatform-Dataverse-Client[async]"
799+
```
800+
801+
### Quick start
802+
803+
```python
804+
import asyncio
805+
from azure.identity.aio import DefaultAzureCredential
806+
from PowerPlatform.Dataverse.aio import AsyncDataverseClient
807+
808+
async def main():
809+
async with DefaultAzureCredential() as credential:
810+
async with AsyncDataverseClient("https://yourorg.crm.dynamics.com", credential) as client:
811+
# Create a contact
812+
contact_id = await client.records.create("contact", {"firstname": "John", "lastname": "Doe"})
813+
814+
# Read it back
815+
contact = await client.records.retrieve("contact", contact_id, select=["firstname", "lastname"])
816+
print(f"Created: {contact['firstname']} {contact['lastname']}")
817+
818+
# Clean up
819+
await client.records.delete("contact", contact_id)
820+
821+
asyncio.run(main())
822+
```
823+
824+
### Standalone usage (without `async with`)
825+
826+
```python
827+
client = AsyncDataverseClient("https://yourorg.crm.dynamics.com", credential)
828+
try:
829+
account_id = await client.records.create("account", {"name": "Contoso Ltd"})
830+
finally:
831+
await client.aclose()
832+
```
833+
834+
### Query builder
835+
836+
The async query builder API is identical to the sync one:
837+
838+
```python
839+
from PowerPlatform.Dataverse.models.filters import col
840+
841+
# Execute and collect all results
842+
result = await (
843+
client.query.builder("account")
844+
.select("name", "telephone1")
845+
.where(col("statecode") == 0)
846+
.top(10)
847+
.execute()
848+
)
849+
for record in result:
850+
print(record["name"])
851+
852+
# Lazy page-by-page iteration (memory-efficient for large sets)
853+
async for page in (
854+
client.query.builder("account")
855+
.select("name")
856+
.page_size(500)
857+
.execute_pages()
858+
):
859+
for record in page:
860+
print(record["name"])
861+
```
862+
863+
### Batch and changesets
864+
865+
```python
866+
batch = client.batch.new()
867+
batch.records.create("account", {"name": "Alpha"})
868+
batch.records.create("account", {"name": "Beta"})
869+
result = await batch.execute()
870+
print(f"Created {len(result.entity_ids)} records")
871+
872+
# Atomic changeset
873+
batch = client.batch.new()
874+
async with batch.changeset() as cs:
875+
ref = cs.records.create("contact", {"firstname": "Alice"})
876+
cs.records.update("account", account_id, {"primarycontactid@odata.bind": ref})
877+
result = await batch.execute()
878+
```
879+
880+
See [examples/aio/](https://github.com/microsoft/PowerPlatform-DataverseClient-Python/tree/main/examples/aio) for async equivalents of all sync examples.
881+
786882
## Next steps
787883

788884
### More sample code
@@ -808,7 +904,7 @@ For comprehensive information on Microsoft Dataverse and related technologies:
808904
| Resource | Description |
809905
|----------|-------------|
810906
| **[Dataverse Developer Guide](https://learn.microsoft.com/power-apps/developer/data-platform/)** | Complete developer documentation for Microsoft Dataverse |
811-
| **[Dataverse Web API Reference](https://learn.microsoft.com/power-apps/developer/data-platform/webapi/)** | Detailed Web API reference and examples |
907+
| **[Dataverse Web API Reference](https://learn.microsoft.com/power-apps/developer/data-platform/webapi/)** | Detailed Web API reference and examples |
812908
| **[Azure Identity for Python](https://learn.microsoft.com/python/api/overview/azure/identity-readme)** | Authentication library documentation and credential types |
813909
| **[Power Platform Developer Center](https://learn.microsoft.com/power-platform/developer/)** | Broader Power Platform development resources |
814910
| **[Dataverse SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/overview)** | Official .NET SDK for Microsoft Dataverse |
@@ -862,8 +958,7 @@ Enable file-based HTTP logging to capture all requests and responses for debuggi
862958

863959
```python
864960
from PowerPlatform.Dataverse.client import DataverseClient
865-
from PowerPlatform.Dataverse.core.config import DataverseConfig
866-
from PowerPlatform.Dataverse.core.log_config import LogConfig
961+
from PowerPlatform.Dataverse.core import DataverseConfig, LogConfig
867962

868963
log_cfg = LogConfig(
869964
log_folder="./my_logs", # Directory for log files (created if missing)

examples/aio/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.

0 commit comments

Comments
 (0)