Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 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
22 changes: 18 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,13 @@ class AsyncDataFrameOperations:
"""

def __init__(self, client: "AsyncDataverseClient") -> None:
try:
import pandas as pd # noqa: F401
Comment thread
ChaitanyaAIML-lab marked this conversation as resolved.
Outdated
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 +94,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 +138,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 +227,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 +301,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
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
29 changes: 27 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,13 @@ class BatchDataFrameOperations:
"""

def __init__(self, batch: "_BatchContext") -> None:
try:
import pandas as pd # noqa: F401
Comment thread
ChaitanyaAIML-lab marked this conversation as resolved.
Outdated
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 +827,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 +880,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 +939,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 +995,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
25 changes: 21 additions & 4 deletions src/PowerPlatform/Dataverse/operations/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@
import warnings
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 ..client import DataverseClient


Expand Down Expand Up @@ -50,6 +47,13 @@ class DataFrameOperations:
"""

def __init__(self, client: DataverseClient) -> None:
try:
import pandas as pd # noqa: F401
Comment thread
ChaitanyaAIML-lab marked this conversation as resolved.
Outdated
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 @@ -89,6 +93,8 @@ def sql(self, sql: str) -> pd.DataFrame:
"GROUP BY a.name"
)
"""
import pandas as pd

rows = self._client.query.sql(sql)
if not rows:
return pd.DataFrame()
Expand Down Expand Up @@ -165,6 +171,8 @@ def get(

df = client.dataframe.get("account", select=["name"], top=100)
"""
import pandas as pd

warnings.warn(
"'dataframe.get()' is deprecated; use "
"client.query.builder(table).where(...).execute().to_dataframe() instead.",
Expand Down Expand Up @@ -247,6 +255,9 @@ def create(
])
df["accountid"] = 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 @@ -333,8 +344,12 @@ def update(
df = pd.DataFrame([{"accountid": "guid-1", "websiteurl": None}])
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:
raise ValueError("changes must be a non-empty DataFrame")
if id_column not in changes.columns:
Expand Down Expand Up @@ -403,6 +418,8 @@ def delete(
ids = pd.Series(["guid-1", "guid-2", "guid-3"])
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
Loading