Skip to content

Commit 567fd61

Browse files
author
Max Wang
committed
add optional parameter to treat None/NaN as clear the field (default is skip)
1 parent aba4cf4 commit 567fd61

7 files changed

Lines changed: 72 additions & 24 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ new_accounts["accountid"] = client.create_dataframe("account", new_accounts)
145145
new_accounts["telephone1"] = ["555-0199", "555-0299"]
146146
client.update_dataframe("account", new_accounts, id_column="accountid")
147147

148+
# Clear a field by setting clear_nulls=True (by default, NaN/None fields are skipped)
149+
df = pd.DataFrame([{"accountid": "guid-1", "websiteurl": None}])
150+
client.update_dataframe("account", df, id_column="accountid", clear_nulls=True)
151+
148152
# Delete records by passing a Series of GUIDs
149153
client.delete_dataframe("account", new_accounts["accountid"])
150154
```

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ new_accounts["accountid"] = client.create_dataframe("account", new_accounts)
207207
new_accounts["telephone1"] = ["555-0199", "555-0299"]
208208
client.update_dataframe("account", new_accounts, id_column="accountid")
209209

210+
# Clear a field by setting clear_nulls=True (by default, NaN/None fields are skipped)
211+
df = pd.DataFrame([{"accountid": ids[0], "websiteurl": None}])
212+
client.update_dataframe("account", df, id_column="accountid", clear_nulls=True)
213+
210214
# Delete records by passing a Series of GUIDs
211215
client.delete_dataframe("account", new_accounts["accountid"])
212216
```

examples/advanced/dataframe_operations.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,19 @@ def main():
124124
verified = next(client.get_dataframe(table, select=select_cols, filter=test_filter))
125125
print(f" Verified:\n{verified.to_string(index=False)}")
126126

127-
# Clear a field by updating to None
128-
print("\n Clearing websiteurl for Contoso by setting to None...")
127+
# Default: NaN/None fields are skipped (not overridden on server)
128+
print("\n Updating with NaN values (default: clear_nulls=False, fields should stay unchanged)...")
129+
sparse_df = pd.DataFrame([
130+
{"accountid": new_accounts["accountid"].iloc[0], "telephone1": "555-9999", "websiteurl": None},
131+
])
132+
client.update_dataframe(table, sparse_df, id_column="accountid")
133+
verified = next(client.get_dataframe(table, select=select_cols, filter=test_filter))
134+
print(f" Verified (Contoso telephone1 updated, websiteurl unchanged):\n{verified.to_string(index=False)}")
135+
136+
# Opt-in: clear_nulls=True sends None as null to clear the field
137+
print("\n Clearing websiteurl for Contoso with clear_nulls=True...")
129138
clear_df = pd.DataFrame([{"accountid": new_accounts["accountid"].iloc[0], "websiteurl": None}])
130-
client.update_dataframe(table, clear_df, id_column="accountid")
139+
client.update_dataframe(table, clear_df, id_column="accountid", clear_nulls=True)
131140
verified = next(client.get_dataframe(table, select=select_cols, filter=test_filter))
132141
print(f" Verified (Contoso websiteurl should be empty):\n{verified.to_string(index=False)}")
133142

src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ new_accounts["accountid"] = client.create_dataframe("account", new_accounts)
145145
new_accounts["telephone1"] = ["555-0199", "555-0299"]
146146
client.update_dataframe("account", new_accounts, id_column="accountid")
147147

148+
# Clear a field by setting clear_nulls=True (by default, NaN/None fields are skipped)
149+
df = pd.DataFrame([{"accountid": "guid-1", "websiteurl": None}])
150+
client.update_dataframe("account", df, id_column="accountid", clear_nulls=True)
151+
148152
# Delete records by passing a Series of GUIDs
149153
client.delete_dataframe("account", new_accounts["accountid"])
150154
```

src/PowerPlatform/Dataverse/client.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ def update_dataframe(
482482
table_schema_name: str,
483483
records: pd.DataFrame,
484484
id_column: str,
485+
clear_nulls: bool = False,
485486
) -> None:
486487
"""
487488
Update records from a pandas DataFrame.
@@ -495,6 +496,11 @@ def update_dataframe(
495496
:type records: ~pd.DataFrame
496497
:param id_column: Name of the DataFrame column containing record GUIDs.
497498
:type id_column: :class:`str`
499+
:param clear_nulls: When ``False`` (default), missing values (NaN/None) are skipped
500+
(the field is left unchanged on the server). When ``True``, missing values are sent
501+
as ``null`` to Dataverse, clearing the field. Use ``True`` only when you intentionally
502+
want NaN/None values to clear fields.
503+
:type clear_nulls: :class:`bool`
498504
499505
:raises TypeError: If ``records`` is not a pandas DataFrame.
500506
:raises ValueError: If ``id_column`` is not found in the DataFrame.
@@ -515,6 +521,11 @@ def update_dataframe(
515521
df = pd.DataFrame({"accountid": ["guid-1", "guid-2", "guid-3"]})
516522
df["websiteurl"] = "https://example.com"
517523
client.update_dataframe("account", df, id_column="accountid")
524+
525+
Clear a field by setting clear_nulls=True::
526+
527+
df = pd.DataFrame([{"accountid": "guid-1", "websiteurl": None}])
528+
client.update_dataframe("account", df, id_column="accountid", clear_nulls=True)
518529
"""
519530
if not isinstance(records, pd.DataFrame):
520531
raise TypeError("records must be a pandas DataFrame")
@@ -523,7 +534,7 @@ def update_dataframe(
523534

524535
ids = records[id_column].tolist()
525536
change_columns = [column for column in records.columns if column != id_column]
526-
changes = dataframe_to_records(records[change_columns])
537+
changes = dataframe_to_records(records[change_columns], na_as_null=clear_nulls)
527538

528539
if len(ids) == 1:
529540
self.update(table_schema_name, ids[0], changes[0])

src/PowerPlatform/Dataverse/utils/_pandas.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@
1010
import pandas as pd
1111

1212

13-
def dataframe_to_records(df: pd.DataFrame) -> List[Dict[str, Any]]:
14-
"""Convert a DataFrame to a list of dicts, converting missing values (e.g. NaN, None, NaT, pd.NA) to None and Timestamps to ISO strings."""
13+
def dataframe_to_records(df: pd.DataFrame, na_as_null: bool = False) -> List[Dict[str, Any]]:
14+
"""Convert a DataFrame to a list of dicts, converting Timestamps to ISO strings.
15+
16+
:param df: Input DataFrame.
17+
:param na_as_null: When False (default), missing values are omitted from each dict.
18+
When True, missing values are included as None (sends null to Dataverse, clearing the field).
19+
"""
1520
records = []
1621
for row in df.to_dict(orient="records"):
1722
clean = {}
1823
for k, v in row.items():
1924
if pd.notna(v):
2025
clean[k] = v.isoformat() if isinstance(v, pd.Timestamp) else v
21-
else:
26+
elif na_as_null:
2227
clean[k] = None
2328
records.append(clean)
2429
return records

tests/unit/test_client_dataframe.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,12 @@ def test_create_empty_dataframe(self):
187187
self.assertIsInstance(ids, pd.Series)
188188
self.assertEqual(len(ids), 0)
189189

190-
def test_create_converts_nan_to_none(self):
191-
"""NaN values are converted to None in the payload."""
192-
df = pd.DataFrame(
193-
[
194-
{"name": "Contoso", "telephone1": "555-0100"},
195-
{"name": "Fabrikam", "telephone1": None},
196-
]
197-
)
190+
def test_create_drops_nan_values(self):
191+
"""NaN/None values are omitted from the create payload."""
192+
df = pd.DataFrame([
193+
{"name": "Contoso", "telephone1": "555-0100"},
194+
{"name": "Fabrikam", "telephone1": None},
195+
])
198196
self.client._odata._create_multiple.return_value = ["guid-1", "guid-2"]
199197
self.client._odata._entity_set_from_schema_name.return_value = "accounts"
200198

@@ -203,7 +201,8 @@ def test_create_converts_nan_to_none(self):
203201
call_args = self.client._odata._create_multiple.call_args
204202
records_arg = call_args[0][2]
205203
self.assertEqual(records_arg[0], {"name": "Contoso", "telephone1": "555-0100"})
206-
self.assertEqual(records_arg[1], {"name": "Fabrikam", "telephone1": None})
204+
self.assertEqual(records_arg[1], {"name": "Fabrikam"})
205+
self.assertNotIn("telephone1", records_arg[1])
207206

208207
def test_create_converts_timestamps_to_iso(self):
209208
"""Timestamp values are converted to ISO 8601 strings."""
@@ -277,17 +276,29 @@ def test_update_multiple_change_columns(self):
277276
self.assertIn("telephone1", changes)
278277
self.assertNotIn("accountid", changes)
279278

280-
def test_update_preserves_none_for_clearing_fields(self):
281-
"""None values in update are kept as None to allow clearing fields in Dataverse."""
282-
df = pd.DataFrame(
283-
[
284-
{"accountid": "guid-1", "name": "New Name", "telephone1": None},
285-
{"accountid": "guid-2", "name": None, "telephone1": "555-0200"},
286-
]
287-
)
279+
def test_update_skips_nan_by_default(self):
280+
"""NaN/None values are skipped by default (field left unchanged on server)."""
281+
df = pd.DataFrame([
282+
{"accountid": "guid-1", "name": "New Name", "telephone1": None},
283+
{"accountid": "guid-2", "name": None, "telephone1": "555-0200"},
284+
])
288285

289286
self.client.update_dataframe("account", df, id_column="accountid")
290287

288+
call_args = self.client._odata._update_by_ids.call_args[0]
289+
changes = call_args[2]
290+
self.assertEqual(changes[0], {"name": "New Name"})
291+
self.assertEqual(changes[1], {"telephone1": "555-0200"})
292+
293+
def test_update_clear_nulls_sends_none(self):
294+
"""With clear_nulls=True, NaN/None values are sent as None to clear fields."""
295+
df = pd.DataFrame([
296+
{"accountid": "guid-1", "name": "New Name", "telephone1": None},
297+
{"accountid": "guid-2", "name": None, "telephone1": "555-0200"},
298+
])
299+
300+
self.client.update_dataframe("account", df, id_column="accountid", clear_nulls=True)
301+
291302
call_args = self.client._odata._update_by_ids.call_args[0]
292303
changes = call_args[2]
293304
self.assertEqual(changes[0], {"name": "New Name", "telephone1": None})

0 commit comments

Comments
 (0)