Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/skills/dataverse-sdk-use/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ client.records.delete("account", [id1, id2, id3], use_bulk_delete=True)

### DataFrame Operations

The SDK provides DataFrame wrappers for all CRUD operations via the `client.dataframe` namespace, using pandas DataFrames and Series as input/output.
The SDK provides DataFrame wrappers for all CRUD operations via the `client.dataframe` namespace, using pandas DataFrames and Series as input/output. Requires the `[dataframe]` extra: `pip install "PowerPlatform-Dataverse-Client[dataframe]"`.

> **Note:** `client.dataframe.get()` is deprecated. Use `client.query.builder(table).select(...).where(...).execute().to_dataframe()` instead. `QueryBuilder.to_dataframe()` (without `.execute()`) is also deprecated — always call `.execute()` first.

Expand Down Expand Up @@ -686,6 +686,8 @@ result = await batch.execute()
```

### DataFrame Operations

Requires the `[dataframe]` extra: `pip install "PowerPlatform-Dataverse-Client[dataframe]"`.
```python
# given: client is an open AsyncDataverseClient
import pandas as pd
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ The Dataverse SDK for Python lets Python developers access, manage, and manipula
pip install PowerPlatform-Dataverse-Client
```

The `pandas` library is installed automatically and powers the `client.dataframe` namespace. The async client requires an optional extra: `pip install "PowerPlatform-Dataverse-Client[async]"`.
The core SDK installs with minimal dependencies. Optional extras:
- DataFrame integration: `pip install "PowerPlatform-Dataverse-Client[dataframe]"`
- Async client: `pip install "PowerPlatform-Dataverse-Client[async]"`
Comment thread
ChaitanyaAIML-lab marked this conversation as resolved.

### Authenticate

Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ dependencies = [
"azure-identity>=1.17.0",
"azure-core>=1.30.2",
"requests>=2.32.0",
"pandas>=2.0.0",
]

[project.urls]
Expand All @@ -46,6 +45,9 @@ dataverse-migrate = "PowerPlatform.Dataverse.migration.migrate_v0_to_v1:main"
async = [
"aiohttp>=3.9",
]
dataframe = [
"pandas>=2.0.0",
]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
Expand All @@ -55,6 +57,7 @@ dev = [
"mypy>=1.0.0",
"ruff>=0.1.0",
"libcst>=1.0.0",
"pandas>=2.0.0",
]
migration = ["libcst>=1.0.0"]

Expand Down
13 changes: 12 additions & 1 deletion src/PowerPlatform/Dataverse/aio/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,20 @@ def __init__(
self.query = AsyncQueryOperations(self)
self.tables = AsyncTableOperations(self)
self.files = AsyncFileOperations(self)
self.dataframe = AsyncDataFrameOperations(self)
self._dataframe: Optional[AsyncDataFrameOperations] = None
self.batch = AsyncBatchOperations(self)

@property
def dataframe(self) -> AsyncDataFrameOperations:
"""pandas DataFrame wrappers for record CRUD operations."""
if self._dataframe is None:
self._dataframe = AsyncDataFrameOperations(self)
return self._dataframe

@dataframe.setter
def dataframe(self, value: AsyncDataFrameOperations) -> None:
self._dataframe = value

def _get_odata(self) -> _AsyncODataClient:
"""
Get or create the internal async OData client instance.
Expand Down
15 changes: 13 additions & 2 deletions src/PowerPlatform/Dataverse/aio/operations/async_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any, List
from typing import TYPE_CHECKING, Any, List, Optional

from ...data._batch_base import _ChangeSet
from ...operations.batch import (
Expand Down Expand Up @@ -103,7 +103,18 @@ def __init__(self, client: "AsyncDataverseClient") -> None:
self.records = BatchRecordOperations(self)
self.tables = BatchTableOperations(self)
self.query = BatchQueryOperations(self)
self.dataframe = BatchDataFrameOperations(self)
self._dataframe: Optional[BatchDataFrameOperations] = None

@property
def dataframe(self) -> BatchDataFrameOperations:
"""pandas DataFrame wrappers for batch operations."""
if self._dataframe is None:
self._dataframe = BatchDataFrameOperations(self)
return self._dataframe

@dataframe.setter
def dataframe(self, value: BatchDataFrameOperations) -> None:
self._dataframe = value

def changeset(self) -> AsyncChangeSet:
"""
Expand Down
24 changes: 20 additions & 4 deletions src/PowerPlatform/Dataverse/aio/operations/async_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@

from typing import TYPE_CHECKING, Any, Dict, List, Optional

import pandas as pd

from ...utils._pandas import dataframe_to_records

if TYPE_CHECKING:
import pandas as pd
from ..async_client import AsyncDataverseClient


Expand Down Expand Up @@ -51,6 +48,15 @@ class AsyncDataFrameOperations:
"""

def __init__(self, client: "AsyncDataverseClient") -> None:
try:
import importlib

importlib.import_module("pandas")
except ImportError as exc:
raise ImportError(
"pandas is required to use DataFrame features. "
"Install it with: pip install 'PowerPlatform-Dataverse-Client[dataframe]'"
) from exc
self._client = client

# --------------------------------------------------------------------- sql
Expand Down Expand Up @@ -90,6 +96,8 @@ async def sql(self, sql: str) -> pd.DataFrame:
"GROUP BY a.name"
)
"""
import pandas as pd

rows = await self._client.query.sql(sql)
if not rows:
return pd.DataFrame()
Expand Down Expand Up @@ -132,6 +140,9 @@ async def create(
])
df["accountid"] = await client.dataframe.create("account", df)
"""
import pandas as pd
from ...utils._pandas import dataframe_to_records

if not isinstance(records, pd.DataFrame):
raise TypeError("records must be a pandas DataFrame")

Expand Down Expand Up @@ -218,6 +229,9 @@ async def update(
df = pd.DataFrame([{"accountid": "guid-1", "websiteurl": None}])
await client.dataframe.update("account", df, id_column="accountid", clear_nulls=True)
"""
import pandas as pd
from ...utils._pandas import dataframe_to_records

if not isinstance(changes, pd.DataFrame):
raise TypeError("changes must be a pandas DataFrame")
if changes.empty:
Expand Down Expand Up @@ -289,6 +303,8 @@ async def delete(
ids = pd.Series(["guid-1", "guid-2", "guid-3"])
await client.dataframe.delete("account", ids)
"""
import pandas as pd

if not isinstance(ids, pd.Series):
raise TypeError("ids must be a pandas Series")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ client.records.delete("account", [id1, id2, id3], use_bulk_delete=True)

### DataFrame Operations

The SDK provides DataFrame wrappers for all CRUD operations via the `client.dataframe` namespace, using pandas DataFrames and Series as input/output.
The SDK provides DataFrame wrappers for all CRUD operations via the `client.dataframe` namespace, using pandas DataFrames and Series as input/output. Requires the `[dataframe]` extra: `pip install "PowerPlatform-Dataverse-Client[dataframe]"`.

> **Note:** `client.dataframe.get()` is deprecated. Use `client.query.builder(table).select(...).where(...).execute().to_dataframe()` instead. `QueryBuilder.to_dataframe()` (without `.execute()`) is also deprecated — always call `.execute()` first.

Expand Down Expand Up @@ -686,6 +686,8 @@ result = await batch.execute()
```

### DataFrame Operations

Requires the `[dataframe]` extra: `pip install "PowerPlatform-Dataverse-Client[dataframe]"`.
```python
# given: client is an open AsyncDataverseClient
import pandas as pd
Expand Down
13 changes: 12 additions & 1 deletion src/PowerPlatform/Dataverse/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,20 @@ def __init__(
self.query = QueryOperations(self)
self.tables = TableOperations(self)
self.files = FileOperations(self)
self.dataframe = DataFrameOperations(self)
self._dataframe: Optional[DataFrameOperations] = None
self.batch = BatchOperations(self)

@property
def dataframe(self) -> DataFrameOperations:
"""pandas DataFrame wrappers for record CRUD operations."""
if self._dataframe is None:
self._dataframe = DataFrameOperations(self)
return self._dataframe

@dataframe.setter
def dataframe(self, value: DataFrameOperations) -> None:
self._dataframe = value

def _get_odata(self) -> _ODataClient:
"""
Get or create the internal OData client instance.
Expand Down
13 changes: 11 additions & 2 deletions src/PowerPlatform/Dataverse/models/query_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

import sys
import warnings
from typing import Any, Iterator, List, Optional, TypedDict, Union
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, TypedDict, Union

# typing.Self (PEP 673, Python 3.11+) makes fluent methods return the concrete
# subclass type. TypeVar fallback for Python 3.10 uses the same name so docs render identically.
Expand All @@ -62,7 +62,8 @@

Self = TypeVar("Self", bound="_QueryBuilderBase") # type: ignore[assignment]

import pandas as pd
if TYPE_CHECKING:
import pandas as pd

from . import filters
from .record import QueryResult, Record
Expand Down Expand Up @@ -666,6 +667,14 @@ def to_dataframe(self) -> pd.DataFrame:
DeprecationWarning,
stacklevel=2,
)
try:
import pandas as pd
except ImportError as exc:
raise ImportError(
"pandas is required for to_dataframe(). "
"Install it with: pip install 'PowerPlatform-Dataverse-Client[dataframe]'"
) from exc

if self._query_ops is None:
raise RuntimeError(
"Cannot execute: query was not created via client.query.builder(). "
Expand Down
5 changes: 4 additions & 1 deletion src/PowerPlatform/Dataverse/models/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ def to_dataframe(self) -> Any:
try:
import pandas as pd
except ImportError as exc:
raise ImportError("pandas is required for to_dataframe(). " "Install it with: pip install pandas") from exc
raise ImportError(
"pandas is required for to_dataframe(). "
"Install it with: pip install 'PowerPlatform-Dataverse-Client[dataframe]'"
) from exc

if not self.records:
return pd.DataFrame()
Expand Down
31 changes: 29 additions & 2 deletions src/PowerPlatform/Dataverse/operations/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import warnings
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Union

import pandas as pd
if TYPE_CHECKING:
import pandas as pd

from ..core.errors import ValidationError
from ..core._error_codes import VALIDATION_SQL_EMPTY
Expand Down Expand Up @@ -798,6 +799,15 @@ class BatchDataFrameOperations:
"""

def __init__(self, batch: "_BatchContext") -> None:
try:
import importlib

importlib.import_module("pandas")
except ImportError as exc:
raise ImportError(
"pandas is required to use DataFrame features. "
"Install it with: pip install 'PowerPlatform-Dataverse-Client[dataframe]'"
) from exc
self._batch = batch

def create(self, table: str, records: pd.DataFrame) -> None:
Expand All @@ -819,6 +829,8 @@ def create(self, table: str, records: pd.DataFrame) -> None:
df = pd.DataFrame([{"name": "Contoso"}, {"name": "Fabrikam"}])
batch.dataframe.create("account", df)
"""
import pandas as pd

if not isinstance(records, pd.DataFrame):
raise TypeError("records must be a pandas DataFrame")
if records.empty:
Expand Down Expand Up @@ -870,6 +882,8 @@ def update(
])
batch.dataframe.update("account", df, id_column="accountid")
"""
import pandas as pd

if not isinstance(changes, pd.DataFrame):
raise TypeError("changes must be a pandas DataFrame")
if changes.empty:
Expand Down Expand Up @@ -927,6 +941,8 @@ def delete(
ids_series = pd.Series(["guid-1", "guid-2", "guid-3"])
batch.dataframe.delete("account", ids_series)
"""
import pandas as pd

if not isinstance(ids, pd.Series):
raise TypeError("ids must be a pandas Series")
raw_list = ids.tolist()
Expand Down Expand Up @@ -981,7 +997,18 @@ def __init__(self, client: "DataverseClient") -> None:
self.records = BatchRecordOperations(self)
self.tables = BatchTableOperations(self)
self.query = BatchQueryOperations(self)
self.dataframe = BatchDataFrameOperations(self)
self._dataframe: Optional[BatchDataFrameOperations] = None

@property
def dataframe(self) -> BatchDataFrameOperations:
"""pandas DataFrame wrappers for batch operations."""
if self._dataframe is None:
self._dataframe = BatchDataFrameOperations(self)
return self._dataframe

@dataframe.setter
def dataframe(self, value: BatchDataFrameOperations) -> None:
self._dataframe = value

def changeset(self) -> ChangeSet:
"""
Expand Down
Loading