Skip to content

Commit 455197a

Browse files
DPGrevclaude
andcommitted
fix: fall back to additional_data when extracting @odata.deltaLink
Matches the .NET SDK's PageIterator, which checks AdditionalData before the strongly-typed OdataDeltaLink property. A custom constructor_callable model that doesn't declare odata_delta_link would otherwise silently lose the delta link, same failure mode as before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent adf9ee6 commit 455197a

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

src/msgraph_core/tasks/page_iterator.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ def __init__(
8383
self._next_link = response.get('odata_next_link', '') if isinstance(
8484
response, dict
8585
) else getattr(response, 'odata_next_link', '')
86-
self._delta_link = response.get('@odata.deltaLink', '') if isinstance(
87-
response, dict
88-
) else getattr(response, 'odata_delta_link', '')
86+
self._delta_link = self._extract_delta_link(response)
8987

9088
if page is not None:
9189
self.current_page = page
@@ -151,14 +149,32 @@ async def next(self) -> Optional[PageResult]:
151149
next_link = response.odata_next_link if response and hasattr(
152150
response, 'odata_next_link'
153151
) else None
154-
delta_link = response.odata_delta_link if response and hasattr(
155-
response, 'odata_delta_link'
156-
) else None
152+
delta_link = self._extract_delta_link(response) if response else None
157153
if delta_link:
158154
self._delta_link = delta_link
159155
value = response.value if response and hasattr(response, 'value') else None
160156
return PageResult(odata_next_link=next_link, value=value)
161157

158+
@staticmethod
159+
def _extract_delta_link(response: Union[T, dict, object]) -> str:
160+
"""
161+
Extracts the '@odata.deltaLink' from a response.
162+
Checks the additional data bag first (for models that do not
163+
explicitly declare the field), then falls back to the typed
164+
'odata_delta_link' attribute.
165+
Args:
166+
response (Union[T, dict, object]): The response to extract the
167+
delta link from.
168+
Returns:
169+
str: The delta link, or an empty string if none is present.
170+
"""
171+
if isinstance(response, dict):
172+
return response.get('@odata.deltaLink', '')
173+
additional_data = getattr(response, 'additional_data', None)
174+
if additional_data and additional_data.get('@odata.deltaLink'):
175+
return additional_data.get('@odata.deltaLink')
176+
return getattr(response, 'odata_delta_link', '')
177+
162178
@staticmethod
163179
def convert_to_page(response: Union[T, list, object]) -> PageResult:
164180
"""

tests/tasks/test_page_iterator.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,39 @@ async def test_delta_link_updated_from_final_page():
125125
assert page_iterator.delta_link == 'https://graph.microsoft.com/v1.0/delta?token=final'
126126

127127

128+
class _CustomPage: # pylint: disable=too-few-public-methods
129+
"""A page response whose model does not declare 'odata_delta_link' and
130+
instead carries it in the additional data bag, like a Kiota-generated
131+
collection response that doesn't model the deltaLink property."""
132+
133+
def __init__(self, value, odata_next_link=None, additional_data=None):
134+
self.value = value
135+
self.odata_next_link = odata_next_link
136+
self.additional_data = additional_data or {}
137+
138+
139+
@pytest.mark.asyncio
140+
async def test_delta_link_falls_back_to_additional_data():
141+
"""Reproduces the gap where a model without a typed 'odata_delta_link'
142+
attribute stores the delta link in additional_data instead."""
143+
first_page = PageResult(odata_next_link='https://graph.microsoft.com/v1.0/next', value=[1, 2])
144+
final_page = _CustomPage(
145+
value=[3, 4],
146+
additional_data={'@odata.deltaLink': 'https://graph.microsoft.com/v1.0/delta?token=final'},
147+
)
148+
149+
adapter = Mock()
150+
adapter.send_async = AsyncMock(return_value=final_page)
151+
152+
page_iterator = PageIterator(first_page, adapter)
153+
154+
items = []
155+
await page_iterator.iterate(lambda item: items.append(item) or True)
156+
157+
assert items == [1, 2, 3, 4]
158+
assert page_iterator.delta_link == 'https://graph.microsoft.com/v1.0/delta?token=final'
159+
160+
128161
@pytest.mark.asyncio
129162
async def test_iterate():
130163
# Mock the next method to return None after the first call

0 commit comments

Comments
 (0)