Skip to content

Commit c812ddf

Browse files
author
Saurabh Badenkal
committed
Complete docstring raises sections, add tests for all-NaN skip, index labels in errors, ID stripping
1 parent 6957488 commit c812ddf

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

src/PowerPlatform/Dataverse/operations/dataframe.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,12 @@ def update(
211211
:type clear_nulls: :class:`bool`
212212
213213
:raises TypeError: If ``changes`` is not a pandas DataFrame.
214-
:raises ValueError: If ``id_column`` is not found in the DataFrame.
214+
:raises ValueError: If ``changes`` is empty, ``id_column`` is not found in the
215+
DataFrame, ``id_column`` contains invalid (non-string, empty, or whitespace-only)
216+
values, or no updatable columns exist besides ``id_column``.
217+
Rows where all change values are NaN/None (after applying ``clear_nulls``)
218+
are silently skipped. If all rows are skipped, the method returns without
219+
making an API call.
215220
216221
.. tip::
217222
All rows are sent in a single ``UpdateMultiple`` request (or a
@@ -294,6 +299,8 @@ def delete(
294299
:type use_bulk_delete: :class:`bool`
295300
296301
:raises TypeError: If ``ids`` is not a pandas Series.
302+
:raises ValueError: If ``ids`` contains invalid (non-string, empty, or
303+
whitespace-only) values.
297304
298305
:return: BulkDelete job ID when deleting multiple records via BulkDelete; otherwise ``None``.
299306
:rtype: :class:`str` or None

tests/unit/test_dataframe_operations.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,49 @@ def test_update_clear_nulls_false(self):
307307
self.assertIn("name", changes)
308308
self.assertNotIn("telephone1", changes)
309309

310+
def test_update_all_nan_rows_skipped(self):
311+
"""When all change values are NaN for every row, no API call is made."""
312+
df = pd.DataFrame(
313+
[
314+
{"accountid": "guid-1", "telephone1": None, "websiteurl": None},
315+
{"accountid": "guid-2", "telephone1": None, "websiteurl": None},
316+
]
317+
)
318+
self.client.dataframe.update("account", df, id_column="accountid")
319+
self.client._odata._update.assert_not_called()
320+
self.client._odata._update_by_ids.assert_not_called()
321+
322+
def test_update_partial_nan_rows_filtered(self):
323+
"""Rows where all changes are NaN are filtered; remaining rows proceed."""
324+
df = pd.DataFrame(
325+
[
326+
{"accountid": "guid-1", "name": "Updated", "telephone1": None},
327+
{"accountid": "guid-2", "name": None, "telephone1": None},
328+
]
329+
)
330+
self.client.dataframe.update("account", df, id_column="accountid")
331+
self.client._odata._update.assert_called_once_with("account", "guid-1", {"name": "Updated"})
332+
333+
def test_update_invalid_ids_reports_index_labels(self):
334+
"""Error message reports DataFrame index labels, not positional indices."""
335+
df = pd.DataFrame(
336+
[
337+
{"accountid": "guid-1", "name": "A"},
338+
{"accountid": None, "name": "B"},
339+
],
340+
index=["row_a", "row_b"],
341+
)
342+
with self.assertRaises(ValueError) as ctx:
343+
self.client.dataframe.update("account", df, id_column="accountid")
344+
self.assertIn("row_b", str(ctx.exception))
345+
346+
def test_update_strips_whitespace_from_ids(self):
347+
"""Leading/trailing whitespace in IDs is stripped before API call."""
348+
df = pd.DataFrame([{"accountid": " guid-1 ", "name": "Contoso"}])
349+
self.client.dataframe.update("account", df, id_column="accountid")
350+
call_args = self.client._odata._update.call_args[0]
351+
self.assertEqual(call_args[1], "guid-1")
352+
310353
def test_update_clear_nulls_true(self):
311354
"""NaN values are sent as None in the update payload when clear_nulls=True."""
312355
df = pd.DataFrame([{"accountid": "guid-1", "name": "New Name", "telephone1": None}])
@@ -368,6 +411,19 @@ def test_delete_with_bulk_delete_false(self):
368411
self.assertIsNone(result)
369412
self.assertEqual(self.client._odata._delete.call_count, 2)
370413

414+
def test_delete_invalid_ids_reports_index_labels(self):
415+
"""Error message reports Series index labels, not positional indices."""
416+
ids = pd.Series(["guid-1", None], index=["row_x", "row_y"])
417+
with self.assertRaises(ValueError) as ctx:
418+
self.client.dataframe.delete("account", ids)
419+
self.assertIn("row_y", str(ctx.exception))
420+
421+
def test_delete_strips_whitespace_from_ids(self):
422+
"""Leading/trailing whitespace in IDs is stripped before API call."""
423+
ids = pd.Series([" guid-1 "])
424+
self.client.dataframe.delete("account", ids)
425+
self.client._odata._delete.assert_called_once_with("account", "guid-1")
426+
371427

372428
class TestDataFrameEndToEnd(unittest.TestCase):
373429
"""End-to-end mocked flow: create -> get -> update -> delete."""

0 commit comments

Comments
 (0)