Skip to content

Add Page Iterator support #479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 76 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
d292cbf
Initialize page_result class
shemogumbe Jan 30, 2024
15664cd
add odata_next_link and value properties
shemogumbe Jan 30, 2024
4bb29cf
add setters for value and odata_next_link
shemogumbe Jan 30, 2024
257978d
add create_from discriminator_value and get_field_deserializer methods
shemogumbe Jan 30, 2024
fb9c548
added get_field_deserializers and serialize method
shemogumbe Jan 30, 2024
b5689ad
expose PageResult to be able to be imported
shemogumbe Jan 30, 2024
2395bed
Add Page Iterator
shemogumbe Jan 30, 2024
c9bbff6
added fetch next link
shemogumbe Jan 30, 2024
c04af55
added next method to the page Iterator
shemogumbe Jan 30, 2024
d139e0b
added convert_to_page method of the Page Iterator
shemogumbe Jan 30, 2024
b7c66e6
Page iterator draft
shemogumbe Jan 30, 2024
c2a3ddd
Use request information, Parsable, headers, request adaoter and method
shemogumbe Jan 31, 2024
3163da1
draft test for page_result model
shemogumbe Feb 1, 2024
401b1ea
Fix set headers
shemogumbe Feb 5, 2024
4a6d391
remove as next method in favor of has next attribute
shemogumbe Feb 5, 2024
64e6a89
fix fetch next page
shemogumbe Feb 5, 2024
86be040
format code
shemogumbe Feb 5, 2024
e3ff67a
remove env files
shemogumbe Feb 5, 2024
9e17e46
remove headers collection import
shemogumbe Feb 5, 2024
4e2fe4f
format code with yapf
shemogumbe Feb 7, 2024
fd532cb
fix linting issus on page iterator
shemogumbe Feb 7, 2024
014146f
debug fetch naxt page
shemogumbe Feb 14, 2024
b76ae84
Fixe fetch next page
shemogumbe Feb 14, 2024
b54e681
Fix keep iterating and enumerate
shemogumbe Feb 14, 2024
d28c10d
code clean up
shemogumbe Feb 14, 2024
7e1a960
fix typing errors
shemogumbe Feb 14, 2024
0e0c802
fix typing issues
shemogumbe Feb 14, 2024
88aca4e
fix typing issues on page iterator
shemogumbe Feb 14, 2024
cfb8458
update types in page iterator
shemogumbe Feb 14, 2024
6b3d311
type check fixes for page iterator
shemogumbe Feb 14, 2024
40eb018
Fix list assignment in page iterator
shemogumbe Feb 14, 2024
e3f4677
Fix list assignment in page iterator
shemogumbe Feb 14, 2024
14ae080
Fix list assignment in page iterator
shemogumbe Feb 14, 2024
e2737e1
Fix list assignment in page iterator
shemogumbe Feb 14, 2024
b40ff3a
Fix list assignment in page iterator
shemogumbe Feb 14, 2024
3b20e13
Fix list assignment in page iterator
shemogumbe Feb 14, 2024
8f87d7f
Fix type checking for page iterator
shemogumbe Feb 14, 2024
4f2677b
Fix type checking for page iterator
shemogumbe Feb 14, 2024
1403cf7
add class and method docstrings
shemogumbe Feb 14, 2024
5f59f94
fix linting issues
shemogumbe Feb 14, 2024
2f49172
fix linting issues
shemogumbe Feb 14, 2024
fc3755e
fix linting issues
shemogumbe Feb 14, 2024
c66ce25
fix failing tests
shemogumbe Feb 14, 2024
6ea49b2
fix failing tests
shemogumbe Feb 14, 2024
f5b5682
Fix unit tests
shemogumbe Feb 19, 2024
eb0d3a1
fix types for page result
shemogumbe Feb 19, 2024
790ebee
fix import errors
shemogumbe Feb 19, 2024
c292706
Add auth credentials as secrets
shemogumbe Feb 19, 2024
7b78122
remove debugging print statements
shemogumbe Feb 21, 2024
ebc53ee
remove debugging print statements
shemogumbe Feb 21, 2024
a0ef95c
Change value to generic type
shemogumbe Feb 23, 2024
9957f03
remove set pause index
shemogumbe Feb 23, 2024
4a13e52
add next link and delta link attributes to page iterator
shemogumbe Feb 23, 2024
219bcef
add next link and delta link attributes to page iterator
shemogumbe Feb 23, 2024
5ca5ed3
add bounds to T
shemogumbe Feb 23, 2024
2f33739
add type annotations to page
shemogumbe Feb 23, 2024
50b245c
add type annotations to page
shemogumbe Feb 23, 2024
58ddea7
fixing linting issues
shemogumbe Feb 23, 2024
4673ead
use type annotations for page result
shemogumbe Mar 5, 2024
8e3d5f3
corect create from descriminator method call
shemogumbe Mar 5, 2024
48e413f
return a headers collection object in set_headers
shemogumbe Mar 5, 2024
0b8c58e
add null check for current page
shemogumbe Mar 5, 2024
884fca4
convert PageResult to dataclass
shemogumbe Mar 14, 2024
d210000
add odata_delta link for last page of next link
shemogumbe Mar 14, 2024
8a84a89
remove hard coded imports
shemogumbe Mar 20, 2024
4db8354
remove multiple type-ignore for imports
shemogumbe Mar 20, 2024
132097f
remove multiple type-ignore for imports
shemogumbe Mar 20, 2024
1d61709
add type requests to dev dependencies
shemogumbe Mar 20, 2024
d66655f
Merge branch 'main' into shem/add-page-iterator
shemogumbe Mar 20, 2024
0b8c586
remove set_value
shemogumbe Mar 20, 2024
13e3878
Merge branch 'shem/add-page-iterator' of github.com:microsoftgraph/ms…
shemogumbe Mar 20, 2024
2202c23
instantiate data class with data points
shemogumbe Mar 20, 2024
0820ca1
Fix tests for changes on data class
shemogumbe Mar 20, 2024
5165ecd
test pagination and iteration
shemogumbe Mar 21, 2024
e1d6313
added type hint in next method
shemogumbe Mar 21, 2024
0eee03e
ignore type checking for page creation at next
shemogumbe Mar 21, 2024
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ venv/
ENV/
env.bak/
venv.bak/
*env/

# Spyder project settings
.spyderproject
Expand All @@ -131,4 +132,4 @@ dmypy.json
# Pycharm
.idea/

app.py
app*.py
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
5 changes: 4 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ pytest-cov==4.1.0

pytest-mock==3.12.0

python-dotenv==1.0.1

pytest-trio==0.8.0

pywin32==306 ; platform_system == 'Windows'
Expand All @@ -110,9 +112,10 @@ trio==0.25.0

types-python-dateutil==2.9.0.20240316

types-requests==2.31.0.20240125; python_version >= '3.7'
urllib3==2.1.0 ; python_version >= '3.7'
typing-extensions==4.10.0 ; python_version >= '3.7'

urllib3==2.2.1 ; python_version >= '3.7'

wrapt==1.15.0 ; python_version < '3.11'

Expand Down
2 changes: 2 additions & 0 deletions src/msgraph_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@
from .authentication import AzureIdentityAuthenticationProvider
from .base_graph_request_adapter import BaseGraphRequestAdapter
from .graph_client_factory import GraphClientFactory
from .tasks import PageIterator
from .models import PageResult

__version__ = SDK_VERSION
3 changes: 3 additions & 0 deletions src/msgraph_core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .page_result import PageResult

__all__ = ['PageResult']
63 changes: 63 additions & 0 deletions src/msgraph_core/models/page_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
This module defines the PageResult class which represents a page of
items in a paged response.

The PageResult class provides methods to get and set the next link and
the items in the page, create a PageResult from a discriminator value, set
the value, get the field deserializers, and serialize the PageResult.

Classes:
PageResult: Represents a page of items in a paged response.
"""
from __future__ import annotations
from typing import List, Optional, Dict, Callable
from dataclasses import dataclass

from kiota_abstractions.serialization.parsable import Parsable
from kiota_abstractions.serialization.serialization_writer \
import SerializationWriter
from kiota_abstractions.serialization.parse_node import ParseNode
from typing import TypeVar, List, Optional

T = TypeVar('T')


@dataclass
class PageResult(Parsable):
odata_next_link: Optional[str] = None
value: Optional[List[Parsable]] = None

@staticmethod
def create_from_discriminator_value(parse_node: Optional[ParseNode] = None) -> PageResult:
"""
Creates a new instance of the appropriate class based on discriminator value
Args:
parseNode: The parse node to use to read the discriminator value and create the object
Returns: Attachment
"""
if not parse_node:
raise TypeError("parse_node cannot be null")
return PageResult()

def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]:
"""Gets the deserialization information for this object.

Returns:
Dict[str, Callable[[ParseNode], None]]: The deserialization information for this
object where each entry is a property key with its deserialization callback.
"""
return {
"@odata.nextLink": lambda x: setattr(self, "odata_next_link", x.get_str_value()),
"value": lambda x: setattr(self, "value", x.get_collection_of_object_values(Parsable))
}

def serialize(self, writer: SerializationWriter) -> None:
"""Writes the objects properties to the current writer.

Args:
writer (SerializationWriter): The writer to write to.
"""
if not writer:
raise TypeError("Writer cannot be null")
writer.write_str_value("@odata.nextLink", self.odata_next_link)
writer.write_collection_of_object_values("value", self.value)
1 change: 1 addition & 0 deletions src/msgraph_core/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .page_iterator import PageIterator
228 changes: 228 additions & 0 deletions src/msgraph_core/tasks/page_iterator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"""
This module contains the PageIterator class which is used to
iterate over paged responses from a server.

The PageIterator class provides methods to iterate over the items
in the pages, fetch the next page, convert a response to a page, and
fetch the next page from the server.

The PageIterator class uses the Parsable interface to parse the responses
from the server, the HttpxRequestAdapter class to send requests to the
server, and the PageResult class to represent the pages.

This module also imports the necessary types and exceptions from the
typing, requests.exceptions, kiota_http.httpx_request_adapter,
kiota_abstractions.method, kiota_abstractions.headers_collection,
kiota_abstractions.request_information, kiota_abstractions.serialization.parsable,
and models modules.
"""

from typing import Callable, Optional, Union, Dict, List

from typing import TypeVar
from requests.exceptions import InvalidURL

from kiota_http.httpx_request_adapter import HttpxRequestAdapter
from kiota_abstractions.method import Method
from kiota_abstractions.headers_collection import HeadersCollection
from kiota_abstractions.request_information import RequestInformation
from kiota_abstractions.serialization.parsable import Parsable

from msgraph_core.models.page_result import PageResult # pylint: disable=no-name-in-module, import-error

T = TypeVar('T', bound=Parsable)


class PageIterator:
"""
This class is used to iterate over paged responses from a server.

The PageIterator class provides methods to iterate over the items in the pages,
fetch the next page, and convert a response to a page.

Attributes:
request_adapter (HttpxRequestAdapter): The adapter used to send HTTP requests.
pause_index (int): The index at which to pause iteration.
headers (HeadersCollection): The headers to include in the HTTP requests.
request_options (list): The options for the HTTP requests.
current_page (PageResult): The current page of items.
object_type (str): The type of the items in the pages.
has_next (bool): Whether there are more pages to fetch.

Methods:
__init__(response: Union[T, list, object], request_adapter: HttpxRequestAdapter,
constructor_callable: Optional[Callable] = None): Initializes a new instance of
the PageIterator class.
"""

def __init__(
self,
response: Union[T, list, object],
request_adapter: HttpxRequestAdapter,
constructor_callable: Optional[Callable] = None
):
self.request_adapter = request_adapter

if isinstance(response, Parsable) and not constructor_callable:
parsable_factory = type(response)
elif constructor_callable is None:
parsable_factory = PageResult
self.parsable_factory = parsable_factory
self.pause_index = 0
self.headers: HeadersCollection = HeadersCollection()
self.request_options = [] # type: ignore
self.current_page = self.convert_to_page(response)
self.object_type = self.current_page.value[
0].__class__.__name__ if self.current_page.value else None
page = self.current_page
self._next_link = response.get('odata_next_link', '') if isinstance(
response, dict
) else getattr(response, 'odata_next_link', '')
self._delta_link = response.get('@odata.deltaLink', '') if isinstance(
response, dict
) else getattr(response, '@odata.deltaLink', '')

if page is not None:
self.current_page = page
self.has_next = bool(page.odata_next_link)

def set_headers(self, headers: dict) -> HeadersCollection:
"""
Sets the headers for the HTTP requests.
This method takes a dictionary of headers and adds them to the
existing headers.
Args:
headers (dict): A dictionary of headers to add. The keys are the
header names and the values are the header values.
"""
self.headers.add_all(**headers)

@property
def delta_link(self):
return self._delta_link

@property
def next_link(self):
return self._next_link

def set_request_options(self, request_options: list) -> None:
"""
Sets the request options for the HTTP requests.
Args:
request_options (list): The request options to set.
"""
self.request_options = request_options

async def iterate(self, callback: Callable) -> None:
"""
Iterates over the pages and applies a callback function to each item.
The iteration stops when there are no more pages or the callback
function returns False.
Args:
callback (Callable): The function to apply to each item.
It should take one argument (the item) and return a boolean.
"""
while True:
keep_iterating = self.enumerate(callback)
if not keep_iterating:
return
next_page = await self.next()
if not next_page:
return
self.current_page = next_page
self.pause_index = 0

async def next(self) -> Optional[PageResult]:
"""
Fetches the next page of items.
Returns:
dict: The next page of items, or None if there are no more pages.
"""
if self.current_page is not None and not self.current_page.odata_next_link:
return None
response = await self.fetch_next_page()
print(f"Response - {type(response)}")
page: PageResult = PageResult(response.odata_next_link, response.value) # type: ignore
return page

@staticmethod
def convert_to_page(response: Union[T, list, object]) -> PageResult:
"""
Converts a response to a PageResult.
This method extracts the 'value' and 'odata_next_link' from the
response and uses them to create a PageResult.
Args:
response (Union[T, list, object]): The response to convert. It can
be a list, an object, or any other type.
Returns:
PageResult: The PageResult created from the response.
Raises:
ValueError: If the response is None or does not contain a 'value'.
"""
if not response:
raise ValueError('Response cannot be null.')
value = None
if isinstance(response, list):
value = response.value # type: ignore
elif hasattr(response, 'value'):
value = getattr(response, 'value')
elif isinstance(response, object):
value = getattr(response, 'value', [])
if value is None:
raise ValueError('The response does not contain a value.')
parsable_page = response if isinstance(response, dict) else vars(response)
next_link = parsable_page.get('odata_next_link', '') if isinstance(
parsable_page, dict
) else getattr(parsable_page, 'odata_next_link', '')

page: PageResult = PageResult(next_link, value)
return page

async def fetch_next_page(self) -> List[Parsable]:
"""
Fetches the next page of items from the server.
Returns:
dict: The response from the server.
Raises:
ValueError: If the current page does not contain a next link.
InvalidURL: If the next link URL could not be parsed.
"""

next_link = self.current_page.odata_next_link
if not next_link:
raise ValueError('The response does not contain a nextLink.')
if not next_link.startswith('http'):
raise InvalidURL('Could not parse nextLink URL.')
request_info = RequestInformation()
request_info.http_method = Method.GET
request_info.url = next_link
request_info.headers = self.headers
if self.request_options:
request_info.add_request_options(*self.request_options)
error_map: Dict[str, int] = {}
response = await self.request_adapter.send_async(
request_info, self.parsable_factory, error_map
)
return response

def enumerate(self, callback: Optional[Callable] = None) -> bool:
"""
Enumerates over the items in the current page and applies a
callback function to each item.
Args:
callback (Callable, optional): The function to apply to each item.
It should take one argument (the item) and return a boolean.
Returns:
bool: False if there are no items in the current page or the
callback function returns False, True otherwise.
"""
keep_iterating = True
page_items = self.current_page.value
if not page_items:
return False
for i in range(self.pause_index, len(page_items)):
keep_iterating = callback(page_items[i]) if callback is not None else True
if not keep_iterating:
self.pause_index = i + 1
break
return keep_iterating
Empty file added tests/tasks/__init__.py
Empty file.
Loading