Skip to content

Commit 05c26a2

Browse files
author
Max Wang
committed
convert NaN to None instead of ignoring them
1 parent c814807 commit 05c26a2

4 files changed

Lines changed: 26 additions & 22 deletions

File tree

examples/advanced/dataframe_operations.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ 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...")
129+
clear_df = pd.DataFrame([{"accountid": new_accounts["accountid"].iloc[0], "websiteurl": None}])
130+
client.update_dataframe(table, clear_df, id_column="accountid")
131+
verified = next(client.get_dataframe(table, select=select_cols, filter=test_filter))
132+
print(f" Verified (Contoso websiteurl should be empty):\n{verified.to_string(index=False)}")
133+
127134
# ── 7. Delete records by passing a Series of GUIDs ────────────
128135
print("\n" + "-" * 60)
129136
print("7. Delete records by passing a Series of GUIDs")

src/PowerPlatform/Dataverse/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ def get_dataframe(
417417
418418
Collect all pages into one DataFrame::
419419
420-
all_data = pd.concat(client.get_dataframe("account", select=["name"]))
420+
all_data = pd.concat(client.get_dataframe("account", select=["name"]), ignore_index=True)
421421
"""
422422
if record_id is not None:
423423
result = self.get(

src/PowerPlatform/Dataverse/utils/_pandas.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111

1212

1313
def dataframe_to_records(df: pd.DataFrame) -> List[Dict[str, Any]]:
14-
"""Convert a DataFrame to a list of dicts, dropping NaN values and converting Timestamps to ISO strings."""
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."""
1515
records = []
1616
for row in df.to_dict(orient="records"):
1717
clean = {}
1818
for k, v in row.items():
1919
if pd.notna(v):
2020
clean[k] = v.isoformat() if isinstance(v, pd.Timestamp) else v
21+
else:
22+
clean[k] = None
2123
records.append(clean)
2224
return records

tests/unit/test_client_dataframe.py

Lines changed: 15 additions & 20 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_drops_nan_values(self):
191-
"""NaN values are stripped from the payload (field omitted, not sent as NaN)."""
192-
df = pd.DataFrame(
193-
[
194-
{"name": "Contoso", "telephone1": "555-0100"},
195-
{"name": "Fabrikam", "telephone1": None},
196-
]
197-
)
190+
def test_create_converts_nan_to_none(self):
191+
"""NaN values are converted to None in the 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,8 +201,7 @@ def test_create_drops_nan_values(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"})
207-
self.assertNotIn("telephone1", records_arg[1])
204+
self.assertEqual(records_arg[1], {"name": "Fabrikam", "telephone1": None})
208205

209206
def test_create_converts_timestamps_to_iso(self):
210207
"""Timestamp values are converted to ISO 8601 strings."""
@@ -278,21 +275,19 @@ def test_update_multiple_change_columns(self):
278275
self.assertIn("telephone1", changes)
279276
self.assertNotIn("accountid", changes)
280277

281-
def test_update_drops_nan_from_changes(self):
282-
"""NaN values in change columns are stripped from the payload."""
283-
df = pd.DataFrame(
284-
[
285-
{"accountid": "guid-1", "name": "New Name", "telephone1": None},
286-
{"accountid": "guid-2", "name": None, "telephone1": "555-0200"},
287-
]
288-
)
278+
def test_update_preserves_none_for_clearing_fields(self):
279+
"""None values in update are kept as None to allow clearing fields in Dataverse."""
280+
df = pd.DataFrame([
281+
{"accountid": "guid-1", "name": "New Name", "telephone1": None},
282+
{"accountid": "guid-2", "name": None, "telephone1": "555-0200"},
283+
])
289284

290285
self.client.update_dataframe("account", df, id_column="accountid")
291286

292287
call_args = self.client._odata._update_by_ids.call_args[0]
293288
changes = call_args[2]
294-
self.assertEqual(changes[0], {"name": "New Name"})
295-
self.assertEqual(changes[1], {"telephone1": "555-0200"})
289+
self.assertEqual(changes[0], {"name": "New Name", "telephone1": None})
290+
self.assertEqual(changes[1], {"name": None, "telephone1": "555-0200"})
296291

297292

298293
class TestDataFrameDelete(unittest.TestCase):

0 commit comments

Comments
 (0)