From c6de11c2e509489d9bd0b8d0ce8dae2c9241aea8 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 13:19:34 +0300 Subject: [PATCH 01/68] add batch request item template --- src/msgraph_core/requests/__init__.py | 0 .../requests/batch_request_builder.py | 0 .../requests/batch_request_content.py | 0 .../batch_request_content_collection.py | 0 .../requests/batch_request_item.py | 52 +++++++++++++++++++ .../requests/batch_response_content.py | 0 .../batch_response_content_collection.py | 0 .../requests/batch_response_item.py | 0 8 files changed, 52 insertions(+) create mode 100644 src/msgraph_core/requests/__init__.py create mode 100644 src/msgraph_core/requests/batch_request_builder.py create mode 100644 src/msgraph_core/requests/batch_request_content.py create mode 100644 src/msgraph_core/requests/batch_request_content_collection.py create mode 100644 src/msgraph_core/requests/batch_request_item.py create mode 100644 src/msgraph_core/requests/batch_response_content.py create mode 100644 src/msgraph_core/requests/batch_response_content_collection.py create mode 100644 src/msgraph_core/requests/batch_response_item.py diff --git a/src/msgraph_core/requests/__init__.py b/src/msgraph_core/requests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py new file mode 100644 index 00000000..e69de29b diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py new file mode 100644 index 00000000..e69de29b diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py new file mode 100644 index 00000000..e69de29b diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py new file mode 100644 index 00000000..894b63f5 --- /dev/null +++ b/src/msgraph_core/requests/batch_request_item.py @@ -0,0 +1,52 @@ +from typing import List, Optional, Dict, Callable + +from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders +from kiota_abstractions.request_information import RequestInformation +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import SerializationWriter + + +class BatchRequestItem(Parsable): + + def __init__( + self, + request_information: RequestInformation, + id: str = "", + depends_on: Optional[List[str]] = None + ): + pass + + def depends_on(self, requests: Optional[List[str]]) -> None: + pass + + def set_url(self, url: str) -> None: + pass + + def get_id(self) -> str: + pass + + @property + def id(self) -> str: + pass + + @property + def headers() -> List[RequestHeaders]: + pass + + @property + def body(self) -> None: + pass + + @property + def method(self) -> str: + pass + + @property + def depends_on(self) -> List[str]: + pass + + def get_field_deserializers(self) -> Dict[str, Callable]: + pass + + def serialize(self, writer: SerializationWriter) -> None: + pass diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py new file mode 100644 index 00000000..e69de29b diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py new file mode 100644 index 00000000..e69de29b diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py new file mode 100644 index 00000000..e69de29b From adfa651bf6dd38a3780a4f9a1047f32d71b1fd65 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 13:28:52 +0300 Subject: [PATCH 02/68] add batch request content template --- .../requests/batch_request_content.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index e69de29b..eaa1f621 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -0,0 +1,61 @@ +from typing import List, Optional, Dict, Callable + +from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders +from kiota_abstractions.request_information import RequestInformation +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import SerializationWriter + + +class BatchRequestContent(Parsable): + """ + Provides operations to call the batch method. + """ + + MAX_REQUESTS = 20 + requests: List = [] + + def __init__(self, requests: Optional[List] = None) -> None: + """ + Initializes a new instance of the BatchRequestContent class. + """ + pass + + @property + def requests(self) -> List: + """ + Gets and sets the requests. + """ + pass + + def add_request(self, request: RequestInformation): + """ + Adds a request to the batch request content. + """ + pass + + def add_request_information(self, selfrequest_information: RequestInformation): + pass + + def remove(seld, request_id: str): + """ + Removes a request from the batch request content. + """ + pass + + def remove_batch_request_item(self, item: BatchRequestItem): + pass + + def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: + """ + The deserialization information for the current model + Returns: Dict[str, Callable[[ParseNode], None]] + """ + pass + + def serialize(self, writer: SerializationWriter) -> None: + """ + Serializes information the current object + Args: + writer: Serialization writer to use to serialize this model + """ + pass From d91546df495067b5251f2be12dfad04540593d46 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 13:29:55 +0300 Subject: [PATCH 03/68] add import for batch request Item --- src/msgraph_core/requests/batch_request_content.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index eaa1f621..28f6a3ca 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -5,6 +5,8 @@ from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import SerializationWriter +from .batch_request_item import BatchRequestItem + class BatchRequestContent(Parsable): """ From f7169eb914076966262f1ca13befc1820ed9095d Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 13:34:15 +0300 Subject: [PATCH 04/68] add Batch request content collection --- .../batch_request_content_collection.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index e69de29b..d1e54f26 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -0,0 +1,23 @@ +class BatchRequestContentCollection: + """A collection of request content objects.""" + + def __init__(self): + self._request_contents = [] + + def add_request_content(self, request_content): + """Add a request content object to the collection. + + Args: + request_content (RequestContent): The request content object to add. + + Returns: + BatchRequestContentCollection: The BatchRequestContentCollection object. + """ + self._request_contents.append(request_content) + return self + + def __iter__(self): + return iter(self._request_contents) + + def __len__(self): + return len(self._request_contents) From 816b797e0ea3e6088a89e51dc61c924e4a786019 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 13:44:10 +0300 Subject: [PATCH 05/68] add template for batch response item --- .../requests/batch_response_item.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index e69de29b..29100a2b 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -0,0 +1,25 @@ +from typing import List, Optional, Dict, Callable + +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import ParseNode +from kiota_abstractions.serialization import SerializationWriter + + +class BatchResponseItem(Parsable): + + id: str = None + status_code: int = None + headers: Dict[str, str] = None + atomicity_group: str = None + + def __init__(self) -> None: + """ + Initializes a new instance of the BatchResponseItem class. + """ + pass + + def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: + pass + + def serialize(self, writer: SerializationWriter) -> None: + pass From 2488b1beb13ea5a8165206f36ddbd52839020813 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 13:55:30 +0300 Subject: [PATCH 06/68] add batch response content template --- .../requests/batch_response_content.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index e69de29b..64a61a32 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -0,0 +1,34 @@ +from typing import List, Optional, Dict, Callable + +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import ParseNode +from kiota_abstractions.serialization import ParseNodeFactory +from kiota_abstractions.serialization import ParseNodeFactoryRegistry +from kiota_abstractions.serialization import SerializationWriter + +from .batch_response_item import BatchResponseItem + + +class BatchResponseContent(Parsable): + responses = [] + + def __init__(self) -> None: + pass + + @property + def responses(self) -> List[Parsable]: + pass + + @property + def response(self, request_id: str) -> BatchResponseItem: + pass + + @property + def response_body(self, request_id: str) -> Optional[Parsable]: + pass + + def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: + pass + + def serialize(self, writer: SerializationWriter) -> None: + pass From dd63461d02d8f67e32d41bf4fa4a2f46e65cd351 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 14:33:22 +0300 Subject: [PATCH 07/68] add batc response content collection template --- .../batch_response_content_collection.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index e69de29b..03d0211f 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -0,0 +1,21 @@ +from typing import List, Optional, Dict, Callable + +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import ParseNode +from kiota_abstractions.serialization import ParseNodeFactory +from kiota_abstractions.serialization import ParseNodeFactoryRegistry +from kiota_abstractions.serialization import SerializationWriter + +from .batch_response_content import BatchResponseContent + + +class BatchResponseContentCollection(Parsable): + + def __init__(self) -> None: + pass + + def get_response(self, request_id: str) -> BatchResponseContent: + pass + + def get_response_status_codes(): + pass From 900bcfd80f84a758aae2eda404da885fdc706bf1 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 16:45:10 +0300 Subject: [PATCH 08/68] add batch request item implementation --- .../requests/batch_request_item.py | 120 +++++++++++++++--- 1 file changed, 102 insertions(+), 18 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 894b63f5..2dbe1f41 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -1,4 +1,11 @@ -from typing import List, Optional, Dict, Callable +import re +import json +from uuid import uuid4 +from typing import List, Optional, Dict, Callable, Union, Any +from io import BytesIO +import base64 +import urllib.request +from urllib.parse import urlparse from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders from kiota_abstractions.request_information import RequestInformation @@ -6,47 +13,124 @@ from kiota_abstractions.serialization import SerializationWriter +class StreamInterface(BytesIO): + pass + + +# request headers and request information are imported, streaIterfac and serialization writer too class BatchRequestItem(Parsable): + API_VERSION_REGEX = re.compile(r'/\/(v1.0|beta)/') + ME_TOKEN_REGEX = re.compile(r'/\/users\/me-token-to-replace/') def __init__( self, request_information: RequestInformation, id: str = "", - depends_on: Optional[List[str]] = None + depends_on: Optional[List[Union[str, 'BatchRequestItem']]] = None ): - pass + if not request_information.http_method: + raise ValueError("HTTP method cannot be Null/Empty") + self._id = id or str(uuid4()) + self.method = request_information.http_method + self._headers = request_information.request_headers + self._body = request_information.content + self.url = request_information.url + self._depends_on: Optional[List[str]] = [] + self.set_depends_on(depends_on) + + @staticmethod + def create_with_urllib_request( + request: urllib.request.Request, + id: str = "", + depends_on: Optional[List[str]] = None + ) -> 'BatchRequestItem': + request_info = RequestInformation() + request_info.http_method = request.get_method() + request_info.url = request.full_url + request_info.headers = dict(request.header_items()) + request_info.content = request.data + return BatchRequestItem(request_info, id, depends_on) - def depends_on(self, requests: Optional[List[str]]) -> None: - pass + def set_depends_on(self, requests: Optional[List[Union[str, 'BatchRequestItem']]]) -> None: + if requests: + for request in requests: + self._depends_on.append(request if isinstance(request, str) else request.id) def set_url(self, url: str) -> None: - pass + url_parts = urlparse(url) + if not url_parts.path: + raise ValueError(f"Invalid URL {url}") - def get_id(self) -> str: - pass + relative_url = re.sub(BatchRequestItem.API_VERSION_REGEX, '', url_parts.path, 1) + if not relative_url: + raise ValueError( + f"Error occurred during regex replacement of API version in URL string: {url}" + ) + + relative_url = re.sub(self.ME_TOKEN_REGEX, '/me', relative_url, 1) + if not relative_url: + raise ValueError( + f"Error occurred during regex replacement of '/users/me-token-to-replace' in URL string: {url}" + ) + + self.url = relative_url + if url_parts.query: + self.url += f"?{url_parts.query}" + if url_parts.fragment: + self.url += f"#{url_parts.fragment}" @property def id(self) -> str: - pass + return self._id + + @id.setter + def id(self, value: str) -> None: + self._id = value @property - def headers() -> List[RequestHeaders]: - pass + def headers(self) -> List[RequestHeaders]: + return self._headers + + @headers.setter + def headers(self, headers: Dict[str, Union[List[str], str]]) -> None: + self._headers.clear() + self._headers.update(headers) @property def body(self) -> None: - pass + return self._body + + @body.setter + def body(self, body: Optional[StreamInterface]) -> None: + self._body = body @property def method(self) -> str: - pass + return self._method + + @method.setter + def method(self, value: str) -> None: + self._method = value @property - def depends_on(self) -> List[str]: - pass + def depends_on(self) -> Optional[List[str]]: + return self._depends_on - def get_field_deserializers(self) -> Dict[str, Callable]: - pass + def get_field_deserializers(self) -> Dict[str, Any]: + return {} def serialize(self, writer: SerializationWriter) -> None: - pass + writer.write_str_value('id', self.id) + writer.write_str_value('method', self.method) + writer.write_str_value('url', self.url) + writer.write_collection_of_primitive_values('depends_on', self._depends_on) + headers = {key: ", ".join(val) for key, val in self._headers.items()} + writer.write_additional_data_value('headers', headers) + if self._body: + json_object = json.loads(self._body.read()) + is_json_string = json_object and isinstance(json_object, dict) + self.body.seek(0) + writer.write_additional_data_value( + 'body', json_object + if is_json_string else base64.b64encode(self._body.read()).decode('utf-8') + ) From 1f0538fa5dd81c9ee88c86e56fc43cd79531c465 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 16:46:44 +0300 Subject: [PATCH 09/68] clean up batch request item implementation --- src/msgraph_core/requests/batch_request_item.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 2dbe1f41..fd20cda6 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -13,11 +13,10 @@ from kiota_abstractions.serialization import SerializationWriter -class StreamInterface(BytesIO): +class StreamInterface(BytesIO): # move to helpers or implement in abstractions pass -# request headers and request information are imported, streaIterfac and serialization writer too class BatchRequestItem(Parsable): API_VERSION_REGEX = re.compile(r'/\/(v1.0|beta)/') ME_TOKEN_REGEX = re.compile(r'/\/users\/me-token-to-replace/') From 7efe4a59a5946f7ebb0f887d692c3a3b4d7e2bd4 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 17:05:33 +0300 Subject: [PATCH 10/68] update batch request content initialization --- src/msgraph_core/requests/batch_request_content.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 28f6a3ca..de14ec88 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -1,4 +1,5 @@ -from typing import List, Optional, Dict, Callable +import uuid +from typing import List, Optional, Dict, Callable, Union from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders from kiota_abstractions.request_information import RequestInformation @@ -16,11 +17,12 @@ class BatchRequestContent(Parsable): MAX_REQUESTS = 20 requests: List = [] - def __init__(self, requests: Optional[List] = None) -> None: + def __init__(self, requests: List[Union['BatchRequestItem', 'RequestInformation']] = []): """ Initializes a new instance of the BatchRequestContent class. """ - pass + self.requests: Dict[str, BatchRequestItem] = {} + self.set_requests(requests) @property def requests(self) -> List: From 75ca8911e4245d96beb6081ed5795ebba5e210c6 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 18:15:00 +0300 Subject: [PATCH 11/68] Add implementation for batch request content --- .../requests/batch_request_content.py | 49 ++++++++++++------- .../requests/batch_request_item.py | 4 +- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index de14ec88..f9af8509 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -1,7 +1,6 @@ import uuid -from typing import List, Optional, Dict, Callable, Union +from typing import List, Dict, Union -from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import SerializationWriter @@ -15,46 +14,58 @@ class BatchRequestContent(Parsable): """ MAX_REQUESTS = 20 - requests: List = [] def __init__(self, requests: List[Union['BatchRequestItem', 'RequestInformation']] = []): """ Initializes a new instance of the BatchRequestContent class. """ - self.requests: Dict[str, BatchRequestItem] = {} - self.set_requests(requests) + self._requests: List[Union[BatchRequestItem, 'RequestInformation']] = requests or [] @property def requests(self) -> List: """ - Gets and sets the requests. + Gets the requests. """ - pass + return self._requests - def add_request(self, request: RequestInformation): + @requests.setter + def requests(self, requests: List[BatchRequestItem]) -> None: + if len(requests) >= BatchRequestContent.MAX_REQUESTS: + raise ValueError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") + for request in requests: + self.add_request(request) + + def add_request(self, request: BatchRequestItem) -> None: """ Adds a request to the batch request content. """ - pass + if len(self.requests) >= BatchRequestContent.MAX_REQUESTS: + raise RuntimeError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") + if not request.id: + request.id = str(uuid.uuid4()) + self._requests.append(request) + + def add_request_information(self, request_information: RequestInformation) -> None: + self.add_request(BatchRequestItem(request_information)) - def add_request_information(self, selfrequest_information: RequestInformation): - pass + def add_urllib_request(self, request) -> None: + self.add_request(BatchRequestItem.create_with_urllib_request(request)) - def remove(seld, request_id: str): + def remove(self, request_id: str) -> None: """ Removes a request from the batch request content. """ - pass + if request_id in self.requests: + del self.requests[request_id] - def remove_batch_request_item(self, item: BatchRequestItem): - pass + def remove_batch_request_item(self, item: BatchRequestItem) -> None: + self.remove(item.id) - def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: + def get_field_deserializers(self, ) -> Dict: """ The deserialization information for the current model - Returns: Dict[str, Callable[[ParseNode], None]] """ - pass + return {} def serialize(self, writer: SerializationWriter) -> None: """ @@ -62,4 +73,4 @@ def serialize(self, writer: SerializationWriter) -> None: Args: writer: Serialization writer to use to serialize this model """ - pass + writer.write_collection_of_object_values("requests", self.requests) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index fd20cda6..d347e1d6 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -46,7 +46,9 @@ def create_with_urllib_request( request_info = RequestInformation() request_info.http_method = request.get_method() request_info.url = request.full_url - request_info.headers = dict(request.header_items()) + request_info.headers = RequestHeaders() + for key, value in request.headers.items(): + request_info.headers.try_add(header_name=key, header_value=value) request_info.content = request.data return BatchRequestItem(request_info, id, depends_on) From 1ec923834fb27fd0810f392ec830e9dd89e29c31 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 3 Sep 2024 19:42:07 +0300 Subject: [PATCH 12/68] Add batch collection implementation --- .../requests/batch_request_content.py | 5 +++ .../batch_request_content_collection.py | 44 ++++++++++++------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index f9af8509..95eb5d32 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -20,6 +20,7 @@ def __init__(self, requests: List[Union['BatchRequestItem', 'RequestInformation' Initializes a new instance of the BatchRequestContent class. """ self._requests: List[Union[BatchRequestItem, 'RequestInformation']] = requests or [] + self.is_finalized = False @property def requests(self) -> List: @@ -61,6 +62,10 @@ def remove(self, request_id: str) -> None: def remove_batch_request_item(self, item: BatchRequestItem) -> None: self.remove(item.id) + def finalize(self): + self.is_finalized = True + return self._requests + def get_field_deserializers(self, ) -> Dict: """ The deserialization information for the current model diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index d1e54f26..6b4bed64 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -1,23 +1,33 @@ -class BatchRequestContentCollection: - """A collection of request content objects.""" +from typing import List + +from kiota_abstractions.request_information import RequestInformation - def __init__(self): - self._request_contents = [] +from .batch_request_content import BatchRequestContent +from .batch_request_item import BatchRequestItem - def add_request_content(self, request_content): - """Add a request content object to the collection. - Args: - request_content (RequestContent): The request content object to add. +class BatchRequestContentCollection: + """A collection of request content objects.""" + + def __init__(self, batch_max_requests: int = 20): + self.batch_max_requests = batch_max_requests or BatchRequestContent.MAX_REQUESTS + self.batches: List[BatchRequestContent] = [] + self.current_batch: BatchRequestContent = BatchRequestContent( + ) # fix, how we get the current batch? - Returns: - BatchRequestContentCollection: The BatchRequestContentCollection object. - """ - self._request_contents.append(request_content) - return self + def add_batch_request_step(self, request: BatchRequestItem) -> None: + try: + self.current_batch.add_request(request) + except ValueError as e: + if "Maximum number of requests is" in str(e): + self.batches.append(self.current_batch.finalize()) - def __iter__(self): - return iter(self._request_contents) + self.current_batch = BatchRequestContent() + self.current_batch.add_request(request) + else: + raise e - def __len__(self): - return len(self._request_contents) + def get_batch_requests_for_execution(self): + if not self.current_batch.is_finalized: + self.batches.append(self.current_batch.finalize()) + return self.batches From b740217f3e6bb10d30d4b3c6ba2f651df4ac386c Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 4 Sep 2024 12:08:39 +0300 Subject: [PATCH 13/68] add batch response item base implementation --- .../requests/batch_response_item.py | 86 ++++++++++++++++--- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 29100a2b..9195d838 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -1,25 +1,91 @@ -from typing import List, Optional, Dict, Callable +from typing import Optional, Dict, Any +from io import BytesIO from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import ParseNode from kiota_abstractions.serialization import SerializationWriter -class BatchResponseItem(Parsable): +class StreamInterface(BytesIO): # move to helpers + pass + - id: str = None - status_code: int = None - headers: Dict[str, str] = None - atomicity_group: str = None +class BatchResponseItem(Parsable): def __init__(self) -> None: """ Initializes a new instance of the BatchResponseItem class. """ - pass + self._id: Optional[str] = None + self._atomicity_group: Optional[str] = None + self._status_code: Optional[int] = None + self._headers: Optional[Dict[str, str]] = {} + self._body: Optional[StreamInterface] = None + + @staticmethod + def create(parse_node: ParseNode) -> 'BatchResponseItem': + return BatchResponseItem() + + @property + def id(self) -> Optional[str]: + return self._id + + @id.setter + def id(self, id: Optional[str]) -> None: + self._id = id + + @property + def atomicity_group(self) -> Optional[str]: + return self._atomicity_group + + @atomicity_group.setter + def atomicity_group(self, atomicity_group: Optional[str]) -> None: + self._atomicity_group = atomicity_group + + @property + def status_code(self) -> Optional[int]: + return self._status_code + + @status_code.setter + def status_code(self, status_code: Optional[int]) -> None: + self._status_code = status_code + + @property + def headers(self) -> Optional[Dict[str, str]]: + return self._headers + + @headers.setter + def headers(self, headers: Optional[Dict[str, str]]) -> None: + self._headers = headers + + @property + def body(self) -> Optional[StreamInterface]: + return self._body + + @body.setter + def body(self, body: Optional[StreamInterface]) -> None: + self._body = body + + @property + def content_type(self) -> Optional[str]: + if self.headers: + headers = {k.lower(): v for k, v in self.headers.items()} + return headers.get('content-type') + return None - def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: - pass + def get_field_deserializers(self) -> Dict[str, Any]: + return { + "@odata.nextLink": lambda x: setattr(self, "odata_next_link", x.get_str_value()), + "id": lambda n: setattr(self, "id", n.get_str_value()), + 'atomicity_group': lambda n: setattr(self, "atomicity_group", n.get_str_value()), + 'status_code': lambda n: setattr(self, "status_code", n.get_int_value()), + 'headers': lambda n: setattr(self, "headers", n.get_collection_of_primitive_values()), + 'body': lambda n: setattr(self, "body", n.get_bytes_value()), + } def serialize(self, writer: SerializationWriter) -> None: - pass + writer.write_str_value('id', self._id) + writer.write_str_value('atomicity_group', self._atomicity_group) + writer.write_int_value('status_code', self._status_code) + writer.write_collection_of_primitive_values('headers', self._headers) + writer.write_bytes_value('body', self._body) From 91e9881588ddcd40bf0a79ac1f9a53b2b22f9282 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 4 Sep 2024 14:51:19 +0300 Subject: [PATCH 14/68] add response collection base implementation --- .../requests/batch_response_content.py | 74 +++++++++++++++---- .../batch_response_content_collection.py | 34 +++++++-- 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 64a61a32..2d68dcb4 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -1,4 +1,6 @@ -from typing import List, Optional, Dict, Callable +from typing import Optional, Dict, Any, Type, TypeVar +from io import BytesIO +import base64 from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import ParseNode @@ -8,27 +10,71 @@ from .batch_response_item import BatchResponseItem +T = TypeVar('T', bound='Parsable') + class BatchResponseContent(Parsable): - responses = [] def __init__(self) -> None: - pass + self._responses: Optional[Dict[str, 'BatchResponseItem']] = {} @property - def responses(self) -> List[Parsable]: - pass + def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: + return None if self._responses is None else self._responses - @property - def response(self, request_id: str) -> BatchResponseItem: - pass + @responses.setter + def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None: + if isinstance(responses, dict): + self._responses = {response.id: response for response in responses.values()} + else: + self._responses = responses - @property - def response_body(self, request_id: str) -> Optional[Parsable]: - pass + def response(self, request_id: str) -> 'BatchResponseItem': + if not self._responses or request_id not in self._responses: + raise ValueError(f"No response found for id: {request_id}") + return self._responses[request_id] + + def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: + if not self._responses or request_id not in self._responses: + raise ValueError(f"No response found for id: {request_id}") - def get_field_deserializers(self, ) -> Dict[str, Callable[[ParseNode], None]]: - pass + if not issubclass(type, Parsable): + raise ValueError("Type passed must implement the Parsable interface") + + response = self._responses[request_id] + content_type = response.content_type + if not content_type: + raise RuntimeError("Unable to get content-type header in response item") + + response_body = response.body or BytesIO() + try: + try: + parse_node = ParseNodeFactoryRegistry().get_root_parse_node( + content_type, response_body + ) + except Exception: + response_body.seek(0) + base64_decoded_body = BytesIO(base64.b64decode(response_body.read())) + parse_node = ParseNodeFactoryRegistry().get_root_parse_node( + content_type, base64_decoded_body + ) + response.body = base64_decoded_body + return parse_node.get_object_value(type.create_from_discriminator_value) + # tests this + except Exception: + raise ValueError( + f"Unable to deserialize batch response for request Id: {request_id} to {type}" + ) + + def get_field_deserializers(self) -> Dict[str, Any]: + return { + 'responses': + lambda n: self.responses(n.get_collection_of_object_values(BatchResponseItem.create)) + } def serialize(self, writer: SerializationWriter) -> None: - pass + writer.write_collection_of_object_values('responses', self._responses.values()) + + @staticmethod + def create_from_discriminator_value(parse_node: ParseNode) -> 'BatchResponseContent': + return BatchResponseContent() diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index 03d0211f..2c4ab0a3 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -7,15 +7,39 @@ from kiota_abstractions.serialization import SerializationWriter from .batch_response_content import BatchResponseContent +from .batch_response_item import BatchResponseItem class BatchResponseContentCollection(Parsable): def __init__(self) -> None: - pass + self._responses: BatchResponseContent = BatchResponseContent() - def get_response(self, request_id: str) -> BatchResponseContent: - pass + def add_response(self, keys: List[str], content: BatchResponseContent): + for key in keys: + self._responses.append({key: content}) # fix this to BatchResponseItem - def get_response_status_codes(): - pass + async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseItem]: + """ + Get a response by its request ID from the collection + :param request_id: The request ID of the response to get + :type request_id: str + :return: The response with the specified request ID as a BatchResponseItem + :rtype: Optional[BatchResponseItem] + """ + if not self._responses: + raise ValueError("No responses found in the collection") + if (isinstance(self._responses, BatchResponseContent)): + for response in self._responses: + if request_id in response: + return self._responses.response(request_id) + raise TypeError("Invalid type: Collection must be of type BatchResponseContent") + + @property + async def responses_status_codes(self) -> Dict[str, int]: + status_codes: Dict[str, int] = {} + for response in self._responses: + if (isinstance(response, BatchResponseItem)): + status_codes[response.id] = response.status_code + raise TypeError("Invalid type: Collection must be of type BatchResponseContent") + return status_codes From e8a8865cb2cc851cab510c12c90f664195489001 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Fri, 6 Sep 2024 15:35:24 +0300 Subject: [PATCH 15/68] fix Response body and response collection data flows --- .../requests/batch_response_content.py | 10 +++--- .../batch_response_content_collection.py | 32 +++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 2d68dcb4..9bf7759f 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Any, Type, TypeVar +from typing import Optional, Dict, Any, Type, TypeVar, Callable from io import BytesIO import base64 @@ -16,7 +16,7 @@ class BatchResponseContent(Parsable): def __init__(self) -> None: - self._responses: Optional[Dict[str, 'BatchResponseItem']] = {} + self._responses: Optional[List['BatchResponseItem']] = [] @property def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: @@ -66,10 +66,12 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: f"Unable to deserialize batch response for request Id: {request_id} to {type}" ) - def get_field_deserializers(self) -> Dict[str, Any]: + def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: return { 'responses': - lambda n: self.responses(n.get_collection_of_object_values(BatchResponseItem.create)) + lambda n: setattr( + self, "_responses", n.get_collection_of_object_values(BatchResponseItem.create) + ) } def serialize(self, writer: SerializationWriter) -> None: diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index 2c4ab0a3..0d7296e7 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -13,11 +13,20 @@ class BatchResponseContentCollection(Parsable): def __init__(self) -> None: + """ + Initializes a new instance of the BatchResponseContentCollection class. + BatchResponseContentCollection is a collection of BatchResponseContent items, each with + a unique request ID. + headers: Optional[Dict[str, str]] = {} + status_code: Optional[int] = None + body: Optional[StreamInterface] = None + + """ self._responses: BatchResponseContent = BatchResponseContent() - def add_response(self, keys: List[str], content: BatchResponseContent): - for key in keys: - self._responses.append({key: content}) # fix this to BatchResponseItem + def add_response(self, content: Optional[BatchResponseItem] = None) -> None: + # loop through items in content adding each to responses + self._responses.responses = [item for item in content] async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseItem]: """ @@ -30,9 +39,9 @@ async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseIte if not self._responses: raise ValueError("No responses found in the collection") if (isinstance(self._responses, BatchResponseContent)): - for response in self._responses: - if request_id in response: - return self._responses.response(request_id) + for response in self._responses.responses: + if isinstance(response, BatchResponseItem) and response.id == request_id: + return response raise TypeError("Invalid type: Collection must be of type BatchResponseContent") @property @@ -43,3 +52,14 @@ async def responses_status_codes(self) -> Dict[str, int]: status_codes[response.id] = response.status_code raise TypeError("Invalid type: Collection must be of type BatchResponseContent") return status_codes + + def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: + return { + 'responses': + lambda n: setattr( + self, "_responses", n.get_collection_of_object_values(BatchResponseItem.create) + ) + } + + def serialize(self, writer: SerializationWriter) -> None: + writer.write_collection_of_object_values('responses', self._responses.values()) From a53936f256a66ad36a3719e659b57a66b0b45ae9 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Fri, 6 Sep 2024 15:47:18 +0300 Subject: [PATCH 16/68] aded docsrtings for response classes and methods --- .../requests/batch_response_content.py | 46 +++++++++++++ .../batch_response_content_collection.py | 22 ++++++- .../requests/batch_response_item.py | 66 ++++++++++++++++++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 9bf7759f..3338d413 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -16,25 +16,55 @@ class BatchResponseContent(Parsable): def __init__(self) -> None: + """ + Initializes a new instance of the BatchResponseContent class. + BatchResponseContent is a collection of BatchResponseItem items, each with a unique request ID. + """ self._responses: Optional[List['BatchResponseItem']] = [] @property def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: + """ + Get the responses in the collection + :return: A dictionary of response IDs and their BatchResponseItem objects + :rtype: Optional[Dict[str, BatchResponseItem]] + """ return None if self._responses is None else self._responses @responses.setter def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None: + """ + Set the responses in the collection + :param responses: The responses to set in the collection + :type responses: Optional[Dict[str, BatchResponseItem]] + """ if isinstance(responses, dict): self._responses = {response.id: response for response in responses.values()} else: self._responses = responses def response(self, request_id: str) -> 'BatchResponseItem': + """ + Get a response by its request ID from the collection + :param request_id: The request ID of the response to get + :type request_id: str + :return: The response with the specified request ID as a BatchResponseItem + :rtype: BatchResponseItem + """ if not self._responses or request_id not in self._responses: raise ValueError(f"No response found for id: {request_id}") return self._responses[request_id] def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: + """ + Get the body of a response by its request ID from the collection + :param request_id: The request ID of the response to get + :type request_id: str + :param type: The type to deserialize the response body to + :type type: Type[T] + :return: The deserialized response body + :rtype: Optional[T] + """ if not self._responses or request_id not in self._responses: raise ValueError(f"No response found for id: {request_id}") @@ -67,6 +97,11 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: ) def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: + """ + Gets the deserialization information for this object. + :return: The deserialization information for this object + :rtype: Dict[str, Callable[[ParseNode], None]] + """ return { 'responses': lambda n: setattr( @@ -75,8 +110,19 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: } def serialize(self, writer: SerializationWriter) -> None: + """ + Writes the objects properties to the current writer. + :param writer: The writer to write to + """ writer.write_collection_of_object_values('responses', self._responses.values()) @staticmethod def create_from_discriminator_value(parse_node: ParseNode) -> 'BatchResponseContent': + """ + Creates a new instance of the appropriate class based on discriminator value + :param parse_node: The parse node to use to read the discriminator value and create the object + :type parse_node: ParseNode + :return: BatchResponseContent + :rtype: BatchResponseContent + """ return BatchResponseContent() diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index 0d7296e7..8865107e 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -25,7 +25,11 @@ def __init__(self) -> None: self._responses: BatchResponseContent = BatchResponseContent() def add_response(self, content: Optional[BatchResponseItem] = None) -> None: - # loop through items in content adding each to responses + """ + Add a response to the collection + :param content: The response to add to the collection + :type content: Optional[BatchResponseItem] + """ self._responses.responses = [item for item in content] async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseItem]: @@ -46,6 +50,11 @@ async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseIte @property async def responses_status_codes(self) -> Dict[str, int]: + """ + Get the status codes of all responses in the collection + :return: A dictionary of response IDs and their status codes + :rtype: Dict[str, int] + """ status_codes: Dict[str, int] = {} for response in self._responses: if (isinstance(response, BatchResponseItem)): @@ -54,6 +63,12 @@ async def responses_status_codes(self) -> Dict[str, int]: return status_codes def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: + """ + Gets the deserialization information for this object. + :return: The deserialization information for this object where each entry is a property key + with its deserialization callback. + :rtype: Dict[str, Callable[[ParseNode], None]] + """ return { 'responses': lambda n: setattr( @@ -62,4 +77,9 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: } def serialize(self, writer: SerializationWriter) -> None: + """ + Writes the objects properties to the current writer. + :param writer: The writer to write to. + :type writer: SerializationWriter + """ writer.write_collection_of_object_values('responses', self._responses.values()) diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 9195d838..5df10ee4 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -24,58 +24,119 @@ def __init__(self) -> None: @staticmethod def create(parse_node: ParseNode) -> 'BatchResponseItem': + """ + Creates a new instance of the BatchResponseItem class. + """ return BatchResponseItem() @property def id(self) -> Optional[str]: + """ + Get the ID of the response + :return: The ID of the response + :rtype: Optional[str] + """ return self._id @id.setter def id(self, id: Optional[str]) -> None: + """ + Set the ID of the response + :param id: The ID of the response + :type id: Optional[str] + """ self._id = id @property def atomicity_group(self) -> Optional[str]: + """ + Get the atomicity group of the response + :return: The atomicity group of the response + :rtype: Optional[str] + """ return self._atomicity_group @atomicity_group.setter def atomicity_group(self, atomicity_group: Optional[str]) -> None: + """ + Set the atomicity group of the response + :param atomicity_group: The atomicity group of the response + :type atomicity_group: Optional[str] + """ self._atomicity_group = atomicity_group @property def status_code(self) -> Optional[int]: + """ + Get the status code of the response + :return: The status code of the response + :rtype: Optional[int] + """ return self._status_code @status_code.setter def status_code(self, status_code: Optional[int]) -> None: + """ + Set the status code of the response + :param status_code: The status code of the response + :type status_code: Optional[int] + """ self._status_code = status_code @property def headers(self) -> Optional[Dict[str, str]]: + """ + Get the headers of the response + :return: The headers of the response + :rtype: Optional[Dict[str, str]] + """ return self._headers @headers.setter def headers(self, headers: Optional[Dict[str, str]]) -> None: + """ + Set the headers of the response + :param headers: The headers of the response + :type headers: Optional[Dict[str, str]] + """ self._headers = headers @property def body(self) -> Optional[StreamInterface]: + """ + Get the body of the response + :return: The body of the response + :rtype: Optional[StreamInterface] + """ return self._body @body.setter def body(self, body: Optional[StreamInterface]) -> None: + """ + Set the body of the response + :param body: The body of the response + :type body: Optional[StreamInterface] + """ self._body = body @property def content_type(self) -> Optional[str]: + """ + Get the content type of the response + :return: The content type of the response + :rtype: Optional[str] + """ if self.headers: headers = {k.lower(): v for k, v in self.headers.items()} return headers.get('content-type') return None def get_field_deserializers(self) -> Dict[str, Any]: + """ + Gets the deserialization information for this object. + + """ return { - "@odata.nextLink": lambda x: setattr(self, "odata_next_link", x.get_str_value()), "id": lambda n: setattr(self, "id", n.get_str_value()), 'atomicity_group': lambda n: setattr(self, "atomicity_group", n.get_str_value()), 'status_code': lambda n: setattr(self, "status_code", n.get_int_value()), @@ -84,6 +145,9 @@ def get_field_deserializers(self) -> Dict[str, Any]: } def serialize(self, writer: SerializationWriter) -> None: + """ + Writes the objects properties to the current writer. + """ writer.write_str_value('id', self._id) writer.write_str_value('atomicity_group', self._atomicity_group) writer.write_int_value('status_code', self._status_code) From 8376c4ce1a686b7a25cdba0016d6654ba523e6fa Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Fri, 6 Sep 2024 18:07:08 +0300 Subject: [PATCH 17/68] fix request content collection creation --- .../batch_request_content_collection.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index 6b4bed64..00cd818e 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from kiota_abstractions.request_information import RequestInformation @@ -9,13 +9,12 @@ class BatchRequestContentCollection: """A collection of request content objects.""" - def __init__(self, batch_max_requests: int = 20): - self.batch_max_requests = batch_max_requests or BatchRequestContent.MAX_REQUESTS - self.batches: List[BatchRequestContent] = [] - self.current_batch: BatchRequestContent = BatchRequestContent( - ) # fix, how we get the current batch? + def __init__(self, batch_request_limit: int = 20): + self.batch_request_limit = batch_request_limit or BatchRequestContent.MAX_REQUESTS + self.batches: [List[BatchRequestContent]] = [] + self.current_batch: BatchRequestContent = BatchRequestContent() - def add_batch_request_step(self, request: BatchRequestItem) -> None: + def add_batch_request_item(self, request: BatchRequestItem) -> None: try: self.current_batch.add_request(request) except ValueError as e: @@ -24,10 +23,30 @@ def add_batch_request_step(self, request: BatchRequestItem) -> None: self.current_batch = BatchRequestContent() self.current_batch.add_request(request) - else: - raise e - - def get_batch_requests_for_execution(self): + self.batches.append(self.current_batch) + + def remove_batch_request_item(self, request_id: str) -> None: + for batch in self.batches: + for request in batch.requests: + if request.id == request_id: + batch.requests.remove(request) + return + for request in self.current_batch.requests: + if request.id == request_id: + self.current_batch.requests.remove(request) + return + + def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: + # Use IDs to get response status codes, generate new batch with failed requests + batch_with_failed_responses: Optional[BatchRequestContent] = BatchRequestContent() + for batch in self.batches: + for request in batch.requests: + if request.status_code != 200: + return batch_with_failed_responses.add_request(request) + return batch_with_failed_responses + + def get_batch_requests_for_execution(self) -> List[BatchRequestContent]: if not self.current_batch.is_finalized: - self.batches.append(self.current_batch.finalize()) + self.current_batch.finalize() + self.batches.append(self.current_batch) return self.batches From 0db2555d3b02eb5ccfeb9dfd65ec735c1b07d945 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Fri, 6 Sep 2024 18:19:51 +0300 Subject: [PATCH 18/68] add doctsrings to atch requst models --- .../requests/batch_request_content.py | 17 ++++ .../batch_request_content_collection.py | 26 ++++++ .../requests/batch_request_item.py | 81 +++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 95eb5d32..b217d0ad 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -31,6 +31,9 @@ def requests(self) -> List: @requests.setter def requests(self, requests: List[BatchRequestItem]) -> None: + """ + Sets the requests. + """ if len(requests) >= BatchRequestContent.MAX_REQUESTS: raise ValueError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") for request in requests: @@ -47,9 +50,17 @@ def add_request(self, request: BatchRequestItem) -> None: self._requests.append(request) def add_request_information(self, request_information: RequestInformation) -> None: + """ + Adds a request to the batch request content. + Args: + request_information (RequestInformation): The request information to add. + """ self.add_request(BatchRequestItem(request_information)) def add_urllib_request(self, request) -> None: + """ + Adds a request to the batch request content. + """ self.add_request(BatchRequestItem.create_with_urllib_request(request)) def remove(self, request_id: str) -> None: @@ -60,9 +71,15 @@ def remove(self, request_id: str) -> None: del self.requests[request_id] def remove_batch_request_item(self, item: BatchRequestItem) -> None: + """ + Removes a request from the batch request content. + """ self.remove(item.id) def finalize(self): + """ + Finalizes the batch request content. + """ self.is_finalized = True return self._requests diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index 00cd818e..b357e180 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -10,11 +10,22 @@ class BatchRequestContentCollection: """A collection of request content objects.""" def __init__(self, batch_request_limit: int = 20): + """ + Initializes a new instance of the BatchRequestContentCollection class. + Args: + batch_request_limit (int, optional): The maximum number of requests in a batch. Defaults to 20. + + """ self.batch_request_limit = batch_request_limit or BatchRequestContent.MAX_REQUESTS self.batches: [List[BatchRequestContent]] = [] self.current_batch: BatchRequestContent = BatchRequestContent() def add_batch_request_item(self, request: BatchRequestItem) -> None: + """ + Adds a request item to the collection. + Args: + request (BatchRequestItem): The request item to add. + """ try: self.current_batch.add_request(request) except ValueError as e: @@ -26,6 +37,11 @@ def add_batch_request_item(self, request: BatchRequestItem) -> None: self.batches.append(self.current_batch) def remove_batch_request_item(self, request_id: str) -> None: + """ + Removes a request item from the collection. + Args: + request_id (str): The ID of the request item to remove. + """ for batch in self.batches: for request in batch.requests: if request.id == request_id: @@ -37,6 +53,11 @@ def remove_batch_request_item(self, request_id: str) -> None: return def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: + """ + Creates a new batch with failed requests. + Returns: + Optional[BatchRequestContent]: A new batch with failed requests. + """ # Use IDs to get response status codes, generate new batch with failed requests batch_with_failed_responses: Optional[BatchRequestContent] = BatchRequestContent() for batch in self.batches: @@ -46,6 +67,11 @@ def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: return batch_with_failed_responses def get_batch_requests_for_execution(self) -> List[BatchRequestContent]: + """ + Gets the batch requests for execution. + Returns: + List[BatchRequestContent]: The batch requests for execution. + """ if not self.current_batch.is_finalized: self.current_batch.finalize() self.batches.append(self.current_batch) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index d347e1d6..cb3ce5c4 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -27,6 +27,13 @@ def __init__( id: str = "", depends_on: Optional[List[Union[str, 'BatchRequestItem']]] = None ): + """ + Initializes a new instance of the BatchRequestItem class. + Args: + request_information (RequestInformation): The request information. + id (str, optional): The ID of the request item. Defaults to "". + depends_on (Optional[List[Union[str, BatchRequestItem]], optional): The IDs of the requests that this request depends on. Defaults to None. + """ if not request_information.http_method: raise ValueError("HTTP method cannot be Null/Empty") self._id = id or str(uuid4()) @@ -43,6 +50,15 @@ def create_with_urllib_request( id: str = "", depends_on: Optional[List[str]] = None ) -> 'BatchRequestItem': + """ + Creates a new instance of the BatchRequestItem class from a urllib request. + Args: + request (urllib.request.Request): The urllib request. + id (str, optional): The ID of the request item. Defaults to "". + depends_on (Optional[List[str]], optional): The IDs of the requests that this request depends on. Defaults to None. + Returns: + BatchRequestItem: A new instance of the BatchRequestItem class. + """ request_info = RequestInformation() request_info.http_method = request.get_method() request_info.url = request.full_url @@ -53,11 +69,21 @@ def create_with_urllib_request( return BatchRequestItem(request_info, id, depends_on) def set_depends_on(self, requests: Optional[List[Union[str, 'BatchRequestItem']]]) -> None: + """ + Sets the IDs of the requests that this request depends on. + Args: + requests (Optional[List[Union[str, BatchRequestItem]]): The IDs of the requests that this request depends on. + """ if requests: for request in requests: self._depends_on.append(request if isinstance(request, str) else request.id) def set_url(self, url: str) -> None: + """ + Sets the URL of the request. + Args: + url (str): The URL of the request. + """ url_parts = urlparse(url) if not url_parts.path: raise ValueError(f"Invalid URL {url}") @@ -82,45 +108,100 @@ def set_url(self, url: str) -> None: @property def id(self) -> str: + """ + Gets the ID of the request item. + Returns: + str: The ID of the request item. + """ return self._id @id.setter def id(self, value: str) -> None: + """ + Sets the ID of the request item. + Args: + value (str): The ID of the request item. + """ self._id = value @property def headers(self) -> List[RequestHeaders]: + """ + Gets the headers of the request item. + Returns: + List[RequestHeaders]: The headers of the request item. + """ return self._headers @headers.setter def headers(self, headers: Dict[str, Union[List[str], str]]) -> None: + """ + Sets the headers of the request item. + Args: + headers (Dict[str, Union[List[str], str]]): The headers of the request item. + """ self._headers.clear() self._headers.update(headers) @property def body(self) -> None: + """ + Gets the body of the request item. + Returns: + None: The body of the request item. + """ return self._body @body.setter def body(self, body: Optional[StreamInterface]) -> None: + """ + Sets the body of the request item. + Args: + body (Optional[StreamInterface]): The body of the request item. + """ self._body = body @property def method(self) -> str: + """ + Gets the HTTP method of the request item. + Returns: + str: The HTTP method of the request item. + """ return self._method @method.setter def method(self, value: str) -> None: + """ + Sets the HTTP method of the request item. + Args: + value (str): The HTTP method of the request item. + """ self._method = value @property def depends_on(self) -> Optional[List[str]]: + """ + Gets the IDs of the requests that this request depends on. + Returns: + Optional[List[str]]: The IDs of the requests that this request depends on. + """ return self._depends_on def get_field_deserializers(self) -> Dict[str, Any]: + """ + Gets the deserialization information for this object. + Returns: + Dict[str, Any]: The deserialization information for this object where each entry is a property key with its deserialization callback. + """ return {} def serialize(self, writer: SerializationWriter) -> None: + """ + Writes the objects properties to the current writer. + Args: + writer (SerializationWriter): The writer to write to. + """ writer.write_str_value('id', self.id) writer.write_str_value('method', self.method) writer.write_str_value('url', self.url) From 84b9b7b754533c4ad4894606384135d7725ba0c1 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 4 Sep 2024 21:00:00 +0300 Subject: [PATCH 19/68] Do a batch request with no body --- .../requests/batch_request_builder.py | 83 +++++++++++++++++++ .../requests/batch_request_item.py | 12 ++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index e69de29b..b9fc77e9 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -0,0 +1,83 @@ +from typing import Dict, Optional, List +import asyncio +import json + +from kiota_abstractions.request_adapter import RequestAdapter +from kiota_abstractions.serialization import ParsableFactory +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.request_information import RequestInformation +from kiota_abstractions.method import Method +from kiota_abstractions.headers_collection import HeadersCollection + +from .batch_request_content import BatchRequestContent +from .batch_request_content_collection import BatchRequestContentCollection +from .batch_response_content import BatchResponseContent +from .batch_response_content_collection import BatchResponseContentCollection + + +class BatchRequestBuilder: + """ + Provides operations to call the batch method. + """ + + def __init__(self, request_adapter: RequestAdapter): + if request_adapter is None: + raise ValueError("request_adapter cannot be Null.") + self._request_adapter = request_adapter + self.url_template = "{}/$batch".format(self._request_adapter.base_url) + + async def post_content( + self, + batch_request_content: BatchRequestContent, + # error_map: Dict[str, int] = {} + ) -> BatchResponseContent: + """ + Sends a batch request and returns the batch response content. + + Args: + batch_request_content (BatchRequestContent): The batch request content. + error_map (Optional[Dict[str, ParsableFactory[Parsable]]]): Error mappings for response handling. + + Returns: + BatchResponseContent: The batch response content. + """ + if batch_request_content is None: + raise ValueError("batch_request_content cannot be Null.") + request_info = await self.to_post_request_information(batch_request_content) + # print(f"Request Info: {request_info}") + print(f"Request Info Content: {request_info.content}") + # print(f"Request Info Headers: {request_info.headers}") + # print(f"Request Info Method: {request_info.http_method}") + # print(f"Request Info URL: {request_info.url}") + # could we use a native response handler here? + parsable_factory = BatchResponseContent() + error_map: Dict[str, int] = {} + response = await self._request_adapter.send_async(request_info, parsable_factory, error_map) + return response + + async def to_post_request_information( + self, + batch_request_content: 'BatchRequestContent', + ) -> RequestInformation: + """ + Creates request information for a batch POST request. + + Args: + batch_request_content (BatchRequestContent): The batch request content. + + Returns: + RequestInformation: The request information. + """ + if batch_request_content is None: + raise ValueError("batch_request_content cannot be Null.") + request_info = RequestInformation() + request_info.http_method = Method.POST + request_info.url_template = self.url_template + # serialized_content = [ + # item.get_field_deserializers() for item in batch_request_content.requests + # ] + # print(f"Serialized Content: {type(serialized_content)}") + # request_info.content = json.dumps(serialized_content).encode("utf-8") + request_info.headers = HeadersCollection() + request_info.headers.try_add("Content-Type", "application/json") + return request_info diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index cb3ce5c4..0b87d0b0 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -188,13 +188,23 @@ def depends_on(self) -> Optional[List[str]]: """ return self._depends_on + def to_dict(self): + return {"id": self.id, "status_code": self.status_code} + def get_field_deserializers(self) -> Dict[str, Any]: """ Gets the deserialization information for this object. Returns: Dict[str, Any]: The deserialization information for this object where each entry is a property key with its deserialization callback. """ - return {} + return { + "id": self._id, + "method": self.method, + "url": self.url, + "headers": self._headers, + "body": self._body, + "depends_on": self._depends_on + } def serialize(self, writer: SerializationWriter) -> None: """ From 1c7e667797af1355fffa7453eab5fd45c7641d25 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 4 Sep 2024 22:30:00 +0300 Subject: [PATCH 20/68] Modify content before posting gives a 400 --- .../requests/batch_request_builder.py | 35 +++++++++++-------- .../requests/batch_request_content.py | 6 +++- .../requests/batch_request_item.py | 17 ++++----- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index b9fc77e9..bb0aa5f7 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, List +from typing import Type, TypeVar, Dict, Optional, Any import asyncio import json @@ -8,12 +8,15 @@ from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.method import Method from kiota_abstractions.headers_collection import HeadersCollection +from kiota_abstractions.api_error import APIError from .batch_request_content import BatchRequestContent from .batch_request_content_collection import BatchRequestContentCollection from .batch_response_content import BatchResponseContent from .batch_response_content_collection import BatchResponseContentCollection +T = TypeVar('T', bound='Parsable') + class BatchRequestBuilder: """ @@ -44,20 +47,24 @@ async def post_content( if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") request_info = await self.to_post_request_information(batch_request_content) - # print(f"Request Info: {request_info}") - print(f"Request Info Content: {request_info.content}") - # print(f"Request Info Headers: {request_info.headers}") - # print(f"Request Info Method: {request_info.http_method}") - # print(f"Request Info URL: {request_info.url}") - # could we use a native response handler here? + request_body = request_info.content.decode("utf-8") + json_body = json.loads(request_body) + request_info.content = json_body + print(f"Request Info Content: {request_body}") + print(f"Request Info Content: {type(request_info.content)}") parsable_factory = BatchResponseContent() error_map: Dict[str, int] = {} - response = await self._request_adapter.send_async(request_info, parsable_factory, error_map) + try: + response = await self._request_adapter.send_async( + request_info, parsable_factory, error_map + ) + except APIError as e: + print(f"API Error: {e}") return response async def to_post_request_information( self, - batch_request_content: 'BatchRequestContent', + batch_request_content: BatchRequestContent, ) -> RequestInformation: """ Creates request information for a batch POST request. @@ -73,11 +80,11 @@ async def to_post_request_information( request_info = RequestInformation() request_info.http_method = Method.POST request_info.url_template = self.url_template - # serialized_content = [ - # item.get_field_deserializers() for item in batch_request_content.requests - # ] - # print(f"Serialized Content: {type(serialized_content)}") - # request_info.content = json.dumps(serialized_content).encode("utf-8") request_info.headers = HeadersCollection() request_info.headers.try_add("Content-Type", "application/json") + request_info.headers.try_add("Accept", "application/json") + request_info.set_content_from_parsable( + self._request_adapter, "application/json", batch_request_content + ) + return request_info diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index b217d0ad..b86e1cab 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -1,5 +1,5 @@ import uuid -from typing import List, Dict, Union +from typing import List, Dict, Union, Any from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.serialization import Parsable @@ -83,6 +83,10 @@ def finalize(self): self.is_finalized = True return self._requests + @classmethod + def create_from_discriminator_value(cls, data: Dict[str, Any]) -> 'BatchResponseContent': + pass + def get_field_deserializers(self, ) -> Dict: """ The deserialization information for the current model diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 0b87d0b0..5615e668 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -153,11 +153,11 @@ def body(self) -> None: return self._body @body.setter - def body(self, body: Optional[StreamInterface]) -> None: + def body(self, body: BytesIO) -> None: """ Sets the body of the request item. Args: - body (Optional[StreamInterface]): The body of the request item. + body : (BytesIO): The body of the request item. """ self._body = body @@ -217,12 +217,13 @@ def serialize(self, writer: SerializationWriter) -> None: writer.write_str_value('url', self.url) writer.write_collection_of_primitive_values('depends_on', self._depends_on) headers = {key: ", ".join(val) for key, val in self._headers.items()} - writer.write_additional_data_value('headers', headers) + writer.write_collection_of_object_values('headers', headers) if self._body: - json_object = json.loads(self._body.read()) + print(f"Body: {self._body}") + json_object = json.loads(self._body) is_json_string = json_object and isinstance(json_object, dict) - self.body.seek(0) - writer.write_additional_data_value( - 'body', json_object - if is_json_string else base64.b64encode(self._body.read()).decode('utf-8') + # self.body.seek(0) + writer.write_collection_of_object_values( + 'body', + json_object if is_json_string else base64.b64encode(self._body).decode('utf-8') ) From 60da07c8fb6bc9a9188459ad7947003a47c332b2 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 12:14:51 +0300 Subject: [PATCH 21/68] run a succesful batc request with content as post --- .../requests/batch_request_builder.py | 12 +++---- .../requests/batch_request_content.py | 14 +++++--- .../requests/batch_request_item.py | 16 ++++++++-- .../requests/batch_response_content.py | 32 ++++++++++++------- .../requests/batch_response_item.py | 10 ++++++ 5 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index bb0aa5f7..32fcf97e 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -32,7 +32,6 @@ def __init__(self, request_adapter: RequestAdapter): async def post_content( self, batch_request_content: BatchRequestContent, - # error_map: Dict[str, int] = {} ) -> BatchResponseContent: """ Sends a batch request and returns the batch response content. @@ -47,13 +46,12 @@ async def post_content( if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") request_info = await self.to_post_request_information(batch_request_content) - request_body = request_info.content.decode("utf-8") - json_body = json.loads(request_body) + content = json.loads(request_info.content.decode("utf-8")) + json_body = json.dumps(content) request_info.content = json_body - print(f"Request Info Content: {request_body}") - print(f"Request Info Content: {type(request_info.content)}") parsable_factory = BatchResponseContent() error_map: Dict[str, int] = {} + response = None try: response = await self._request_adapter.send_async( request_info, parsable_factory, error_map @@ -80,9 +78,11 @@ async def to_post_request_information( request_info = RequestInformation() request_info.http_method = Method.POST request_info.url_template = self.url_template + requests_dict = [item.get_field_deserializers() for item in batch_request_content.requests] + request_info.content = json.dumps({"requests": requests_dict}).encode("utf-8") + request_info.headers = HeadersCollection() request_info.headers.try_add("Content-Type", "application/json") - request_info.headers.try_add("Accept", "application/json") request_info.set_content_from_parsable( self._request_adapter, "application/json", batch_request_content ) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index b86e1cab..6f877709 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -1,8 +1,8 @@ import uuid -from typing import List, Dict, Union, Any +from typing import List, Dict, Union, Any, Optional from kiota_abstractions.request_information import RequestInformation -from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import Parsable, ParseNode from kiota_abstractions.serialization import SerializationWriter from .batch_request_item import BatchRequestItem @@ -83,9 +83,13 @@ def finalize(self): self.is_finalized = True return self._requests - @classmethod - def create_from_discriminator_value(cls, data: Dict[str, Any]) -> 'BatchResponseContent': - pass + @staticmethod + def create_from_discriminator_value( + parse_node: Optional[ParseNode] = None + ) -> 'BatchRequestContent': + if parse_node is None: + raise ValueError("parse_node cannot be None") + return BatchRequestContent() def get_field_deserializers(self, ) -> Dict: """ diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 5615e668..bd62778f 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -11,6 +11,7 @@ from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import SerializationWriter +from kiota_abstractions.serialization import ParseNode class StreamInterface(BytesIO): # move to helpers or implement in abstractions @@ -188,8 +189,19 @@ def depends_on(self) -> Optional[List[str]]: """ return self._depends_on - def to_dict(self): - return {"id": self.id, "status_code": self.status_code} + @staticmethod + def create_from_discriminator_value( + parse_node: Optional[ParseNode] = None + ) -> 'BatchRequestItem': + """ + Creates a new instance of the appropriate class based + on discriminator value param parse_node: The parse node + to use to read the discriminator value and create the object + Returns: BatchRequestItem + """ + if not parse_node: + raise TypeError("parse_node cannot be null.") + return BatchRequestItem() def get_field_deserializers(self) -> Dict[str, Any]: """ diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 3338d413..dad8b1af 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -96,6 +96,14 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: f"Unable to deserialize batch response for request Id: {request_id} to {type}" ) + @staticmethod + def create_from_discriminator_value( + parse_node: Optional[ParseNode] = None + ) -> 'BatchResponseContent': + if parse_node is None: + raise ValueError("parse_node cannot be None") + return BatchResponseContent() + def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """ Gets the deserialization information for this object. @@ -103,10 +111,13 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: :rtype: Dict[str, Callable[[ParseNode], None]] """ return { - 'responses': - lambda n: setattr( - self, "_responses", n.get_collection_of_object_values(BatchResponseItem.create) - ) + # 'responses': + # lambda n: setattr( + # self, "_responses", + # n.get_collection_of_object_values( + # BatchResponseItem.create_from_discriminator_value(parse_node=JsonParseNode()) + # ) + # ) } def serialize(self, writer: SerializationWriter) -> None: @@ -117,12 +128,9 @@ def serialize(self, writer: SerializationWriter) -> None: writer.write_collection_of_object_values('responses', self._responses.values()) @staticmethod - def create_from_discriminator_value(parse_node: ParseNode) -> 'BatchResponseContent': - """ - Creates a new instance of the appropriate class based on discriminator value - :param parse_node: The parse node to use to read the discriminator value and create the object - :type parse_node: ParseNode - :return: BatchResponseContent - :rtype: BatchResponseContent - """ + def create_from_discriminator_value( + parse_node: Optional[ParseNode] = None + ) -> 'BatchResponseContent': + if parse_node is None: + raise ValueError("parse_node cannot be None") return BatchResponseContent() diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 5df10ee4..eb685d6f 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -131,6 +131,16 @@ def content_type(self) -> Optional[str]: return headers.get('content-type') return None + @staticmethod + def create_from_discriminator_value( + parse_node: Optional[ParseNode] = None + ) -> 'BatchResponseItem': + if parse_node is None: + raise ValueError("parse_node cannot be None") + return BatchResponseItem( + id=parse_node.get_str_value("id"), status=parse_node.get_int_value("status") + ) + def get_field_deserializers(self) -> Dict[str, Any]: """ Gets the deserialization information for this object. From 111ad991aaa75d5dad4a146954078b785ec05ada Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 13:58:29 +0300 Subject: [PATCH 22/68] fix deserialization --- .../requests/batch_request_builder.py | 4 +++- .../requests/batch_response_content.py | 18 +++++++----------- .../requests/batch_response_item.py | 9 ++++++--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 32fcf97e..750eed39 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -9,6 +9,7 @@ from kiota_abstractions.method import Method from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.api_error import APIError +from kiota_serialization_json.json_parse_node import JsonParseNode from .batch_request_content import BatchRequestContent from .batch_request_content_collection import BatchRequestContentCollection @@ -54,10 +55,11 @@ async def post_content( response = None try: response = await self._request_adapter.send_async( - request_info, parsable_factory, error_map + request_info, BatchResponseContent, error_map ) except APIError as e: print(f"API Error: {e}") + print(f"Response before processing: {response}") return response async def to_post_request_information( diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index dad8b1af..7347281c 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -8,6 +8,7 @@ from kiota_abstractions.serialization import ParseNodeFactoryRegistry from kiota_abstractions.serialization import SerializationWriter +from kiota_serialization_json.json_parse_node import JsonParseNode from .batch_response_item import BatchResponseItem T = TypeVar('T', bound='Parsable') @@ -29,7 +30,7 @@ def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: :return: A dictionary of response IDs and their BatchResponseItem objects :rtype: Optional[Dict[str, BatchResponseItem]] """ - return None if self._responses is None else self._responses + return self._responses or None @responses.setter def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None: @@ -100,8 +101,8 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: def create_from_discriminator_value( parse_node: Optional[ParseNode] = None ) -> 'BatchResponseContent': - if parse_node is None: - raise ValueError("parse_node cannot be None") + # if parse_node is None: + # raise ValueError("parse_node cannot be None") return BatchResponseContent() def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: @@ -111,13 +112,8 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: :rtype: Dict[str, Callable[[ParseNode], None]] """ return { - # 'responses': - # lambda n: setattr( - # self, "_responses", - # n.get_collection_of_object_values( - # BatchResponseItem.create_from_discriminator_value(parse_node=JsonParseNode()) - # ) - # ) + 'responses': + lambda n: self.responses(n.get_collection_of_object_values(BatchResponseItem.create)) } def serialize(self, writer: SerializationWriter) -> None: @@ -125,7 +121,7 @@ def serialize(self, writer: SerializationWriter) -> None: Writes the objects properties to the current writer. :param writer: The writer to write to """ - writer.write_collection_of_object_values('responses', self._responses.values()) + writer.write_collection_of_object_values('responses', self._responses) @staticmethod def create_from_discriminator_value( diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index eb685d6f..082d3c4d 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -135,10 +135,13 @@ def content_type(self) -> Optional[str]: def create_from_discriminator_value( parse_node: Optional[ParseNode] = None ) -> 'BatchResponseItem': - if parse_node is None: - raise ValueError("parse_node cannot be None") + # if parse_node is None: + # raise ValueError("parse_node cannot be None") return BatchResponseItem( - id=parse_node.get_str_value("id"), status=parse_node.get_int_value("status") + id=parse_node.get_str_value("id"), + status=parse_node.get_int_value("status"), + headers=parse_node.get_object_value(lambda n: n.get_str_value()), + body=parse_node.get_object_value(lambda n: n.get_object_value(lambda x: x)) ) def get_field_deserializers(self) -> Dict[str, Any]: From 856a1bb263c2afd1a2b2ba4b665f55b0e39e5675 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 15:55:19 +0300 Subject: [PATCH 23/68] fix response serialization --- .../requests/batch_request_item.py | 2 +- .../requests/batch_response_content.py | 3 +- .../requests/batch_response_item.py | 48 +++++++++---------- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index bd62778f..e94545ab 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -14,7 +14,7 @@ from kiota_abstractions.serialization import ParseNode -class StreamInterface(BytesIO): # move to helpers or implement in abstractions +class StreamInterface(BytesIO): pass diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 7347281c..88fce235 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -113,7 +113,8 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """ return { 'responses': - lambda n: self.responses(n.get_collection_of_object_values(BatchResponseItem.create)) + lambda n: + setattr(self, 'responses', n.get_collection_of_object_values(BatchResponseItem)) } def serialize(self, writer: SerializationWriter) -> None: diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 082d3c4d..255f23cf 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -1,12 +1,14 @@ from typing import Optional, Dict, Any from io import BytesIO -from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import Parsable, ParsableFactory from kiota_abstractions.serialization import ParseNode +from kiota_serialization_json.json_parse_node import JsonParseNode from kiota_abstractions.serialization import SerializationWriter +from kiota_serialization_json.json_parse_node_factory import JsonParseNodeFactory -class StreamInterface(BytesIO): # move to helpers +class StreamInterface(BytesIO): pass @@ -18,17 +20,10 @@ def __init__(self) -> None: """ self._id: Optional[str] = None self._atomicity_group: Optional[str] = None - self._status_code: Optional[int] = None + self._status: Optional[int] = None self._headers: Optional[Dict[str, str]] = {} self._body: Optional[StreamInterface] = None - @staticmethod - def create(parse_node: ParseNode) -> 'BatchResponseItem': - """ - Creates a new instance of the BatchResponseItem class. - """ - return BatchResponseItem() - @property def id(self) -> Optional[str]: """ @@ -66,7 +61,7 @@ def atomicity_group(self, atomicity_group: Optional[str]) -> None: self._atomicity_group = atomicity_group @property - def status_code(self) -> Optional[int]: + def status(self) -> Optional[int]: """ Get the status code of the response :return: The status code of the response @@ -74,8 +69,8 @@ def status_code(self) -> Optional[int]: """ return self._status_code - @status_code.setter - def status_code(self, status_code: Optional[int]) -> None: + @status.setter + def status(self, status_code: Optional[int]) -> None: """ Set the status code of the response :param status_code: The status code of the response @@ -135,14 +130,15 @@ def content_type(self) -> Optional[str]: def create_from_discriminator_value( parse_node: Optional[ParseNode] = None ) -> 'BatchResponseItem': - # if parse_node is None: - # raise ValueError("parse_node cannot be None") - return BatchResponseItem( - id=parse_node.get_str_value("id"), - status=parse_node.get_int_value("status"), - headers=parse_node.get_object_value(lambda n: n.get_str_value()), - body=parse_node.get_object_value(lambda n: n.get_object_value(lambda x: x)) - ) + """ + Creates a new instance of the appropriate class based on discriminator value + Args: + parse_node: The parse node to use to read the discriminator value and create the object + Returns: BatchResponseItem + """ + if not parse_node: + raise TypeError("parse_node cannot be null") + return BatchResponseItem() def get_field_deserializers(self) -> Dict[str, Any]: """ @@ -150,11 +146,11 @@ def get_field_deserializers(self) -> Dict[str, Any]: """ return { - "id": lambda n: setattr(self, "id", n.get_str_value()), - 'atomicity_group': lambda n: setattr(self, "atomicity_group", n.get_str_value()), - 'status_code': lambda n: setattr(self, "status_code", n.get_int_value()), - 'headers': lambda n: setattr(self, "headers", n.get_collection_of_primitive_values()), - 'body': lambda n: setattr(self, "body", n.get_bytes_value()), + "id": lambda x: setattr(self, "id", x.get_str_value()), + "status": lambda x: setattr(self, "status", x.get_int_value()), + "headers": + lambda x: setattr(self, "headers", x.get_collection_of_primitive_values(str)), + "body": lambda x: setattr(self, "body", x.get_collection_of_primitive_values(str)), } def serialize(self, writer: SerializationWriter) -> None: From 543f1094040de5cca2c0ed0c73314ddf7951f251 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 16:56:36 +0300 Subject: [PATCH 24/68] fix code format --- .../requests/batch_request_builder.py | 2 ++ .../requests/batch_request_content.py | 7 ++-- .../batch_request_content_collection.py | 7 ++-- .../requests/batch_request_item.py | 11 +++--- .../requests/batch_response_content.py | 36 ++++++++++--------- .../batch_response_content_collection.py | 19 +++++++--- .../requests/batch_response_item.py | 6 ++-- 7 files changed, 55 insertions(+), 33 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 750eed39..0371960a 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -59,6 +59,8 @@ async def post_content( ) except APIError as e: print(f"API Error: {e}") + if response is None: + raise ValueError("Failed to get a valid response from the API.") print(f"Response before processing: {response}") return response diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 6f877709..2518a871 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -67,8 +67,11 @@ def remove(self, request_id: str) -> None: """ Removes a request from the batch request content. """ - if request_id in self.requests: - del self.requests[request_id] + for request in self.requests: + if request.id == request_id: + self.requests.remove(request) + return + raise ValueError(f"Request ID {request_id} not found in requests.") def remove_batch_request_item(self, item: BatchRequestItem) -> None: """ diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index b357e180..07aace87 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -17,7 +17,7 @@ def __init__(self, batch_request_limit: int = 20): """ self.batch_request_limit = batch_request_limit or BatchRequestContent.MAX_REQUESTS - self.batches: [List[BatchRequestContent]] = [] + self.batches: List[BatchRequestContent] = [] self.current_batch: BatchRequestContent = BatchRequestContent() def add_batch_request_item(self, request: BatchRequestItem) -> None: @@ -63,7 +63,10 @@ def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: for batch in self.batches: for request in batch.requests: if request.status_code != 200: - return batch_with_failed_responses.add_request(request) + if batch_with_failed_responses is not None: + batch_with_failed_responses.add_request(request) + else: + raise ValueError("batch_with_failed_responses is None") return batch_with_failed_responses def get_batch_requests_for_execution(self) -> List[BatchRequestContent]: diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index e94545ab..90d168b1 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -24,9 +24,9 @@ class BatchRequestItem(Parsable): def __init__( self, - request_information: RequestInformation, + request_information: Optional[RequestInformation] = None, id: str = "", - depends_on: Optional[List[Union[str, 'BatchRequestItem']]] = None + depends_on: Optional[List[Union[str, 'BatchRequestItem']]] = [] ): """ Initializes a new instance of the BatchRequestItem class. @@ -35,7 +35,7 @@ def __init__( id (str, optional): The ID of the request item. Defaults to "". depends_on (Optional[List[Union[str, BatchRequestItem]], optional): The IDs of the requests that this request depends on. Defaults to None. """ - if not request_information.http_method: + if request_information is None or not request_information.http_method: raise ValueError("HTTP method cannot be Null/Empty") self._id = id or str(uuid4()) self.method = request_information.http_method @@ -43,7 +43,8 @@ def __init__( self._body = request_information.content self.url = request_information.url self._depends_on: Optional[List[str]] = [] - self.set_depends_on(depends_on) + if depends_on is not None: + self.set_depends_on(depends_on) @staticmethod def create_with_urllib_request( @@ -77,6 +78,8 @@ def set_depends_on(self, requests: Optional[List[Union[str, 'BatchRequestItem']] """ if requests: for request in requests: + if self._depends_on is None: + self._depends_on = [] self._depends_on.append(request if isinstance(request, str) else request.id) def set_url(self, url: str) -> None: diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 88fce235..149ea063 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Any, Type, TypeVar, Callable +from typing import Optional, Dict, List, Any, Type, TypeVar, Callable from io import BytesIO import base64 @@ -24,16 +24,16 @@ def __init__(self) -> None: self._responses: Optional[List['BatchResponseItem']] = [] @property - def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: + def responses(self) -> Optional[List['BatchResponseItem']]: """ Get the responses in the collection :return: A dictionary of response IDs and their BatchResponseItem objects :rtype: Optional[Dict[str, BatchResponseItem]] """ - return self._responses or None + return self._responses @responses.setter - def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None: + def responses(self, responses: Optional[List['BatchResponseItem']]) -> None: """ Set the responses in the collection :param responses: The responses to set in the collection @@ -52,9 +52,20 @@ def response(self, request_id: str) -> 'BatchResponseItem': :return: The response with the specified request ID as a BatchResponseItem :rtype: BatchResponseItem """ - if not self._responses or request_id not in self._responses: - raise ValueError(f"No response found for id: {request_id}") - return self._responses[request_id] + if self._responses is None: + raise ValueError("Responses list is not initialized.") + for response in self._responses: + if response.request_id == request_id: + return response + raise KeyError(f"Response with request ID {request_id} not found.") + + def get_response_by_id(self, request_id: str) -> 'BatchResponseItem': + if self._responses is None: + raise ValueError("Responses list is not initialized.") + for response in self._responses: + if response.request_id == request_id: + return response + raise KeyError(f"Response with request ID {request_id} not found.") def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: """ @@ -72,7 +83,7 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: if not issubclass(type, Parsable): raise ValueError("Type passed must implement the Parsable interface") - response = self._responses[request_id] + response = self.get_response_by_id(request_id) content_type = response.content_type if not content_type: raise RuntimeError("Unable to get content-type header in response item") @@ -91,20 +102,11 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: ) response.body = base64_decoded_body return parse_node.get_object_value(type.create_from_discriminator_value) - # tests this except Exception: raise ValueError( f"Unable to deserialize batch response for request Id: {request_id} to {type}" ) - @staticmethod - def create_from_discriminator_value( - parse_node: Optional[ParseNode] = None - ) -> 'BatchResponseContent': - # if parse_node is None: - # raise ValueError("parse_node cannot be None") - return BatchResponseContent() - def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """ Gets the deserialization information for this object. diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index 8865107e..5a890f96 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -30,7 +30,10 @@ def add_response(self, content: Optional[BatchResponseItem] = None) -> None: :param content: The response to add to the collection :type content: Optional[BatchResponseItem] """ - self._responses.responses = [item for item in content] + if content is None: + return + for item in content: + self._responses.add_response(item) async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseItem]: """ @@ -42,7 +45,9 @@ async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseIte """ if not self._responses: raise ValueError("No responses found in the collection") - if (isinstance(self._responses, BatchResponseContent)): + if isinstance(self._responses, BatchResponseContent): + if self._responses.responses is None: + raise ValueError("No responses found in the collection") for response in self._responses.responses: if isinstance(response, BatchResponseItem) and response.id == request_id: return response @@ -57,9 +62,13 @@ async def responses_status_codes(self) -> Dict[str, int]: """ status_codes: Dict[str, int] = {} for response in self._responses: - if (isinstance(response, BatchResponseItem)): - status_codes[response.id] = response.status_code - raise TypeError("Invalid type: Collection must be of type BatchResponseContent") + if isinstance(response, BatchResponseItem): + if response.id is not None: + status_codes[response.id] = response.status_code + else: + raise ValueError("Response ID cannot be None") + else: + raise TypeError("Invalid type: Collection must be of type BatchResponseContent") return status_codes def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 255f23cf..9fc4a468 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -22,7 +22,7 @@ def __init__(self) -> None: self._atomicity_group: Optional[str] = None self._status: Optional[int] = None self._headers: Optional[Dict[str, str]] = {} - self._body: Optional[StreamInterface] = None + self._body: Optional[BytesIO] = None @property def id(self) -> Optional[str]: @@ -97,11 +97,11 @@ def headers(self, headers: Optional[Dict[str, str]]) -> None: self._headers = headers @property - def body(self) -> Optional[StreamInterface]: + def body(self) -> Optional[BytesIO]: """ Get the body of the response :return: The body of the response - :rtype: Optional[StreamInterface] + :rtype: Optional[BytesIO] """ return self._body From b8f597bd8a47b4673c09e86aab86e827149fc0db Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:07:05 +0300 Subject: [PATCH 25/68] remove graph dependencies --- src/msgraph_core/requests/batch_response_item.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 9fc4a468..01e7c768 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -3,9 +3,7 @@ from kiota_abstractions.serialization import Parsable, ParsableFactory from kiota_abstractions.serialization import ParseNode -from kiota_serialization_json.json_parse_node import JsonParseNode from kiota_abstractions.serialization import SerializationWriter -from kiota_serialization_json.json_parse_node_factory import JsonParseNodeFactory class StreamInterface(BytesIO): From 4193f4ab62dbb67f8d5e3cbd248beaef216baebf Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:09:05 +0300 Subject: [PATCH 26/68] remove sdk dependencies --- src/msgraph_core/requests/batch_request_builder.py | 1 - src/msgraph_core/requests/batch_response_content.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 0371960a..fb0c74e9 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -9,7 +9,6 @@ from kiota_abstractions.method import Method from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.api_error import APIError -from kiota_serialization_json.json_parse_node import JsonParseNode from .batch_request_content import BatchRequestContent from .batch_request_content_collection import BatchRequestContentCollection diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 149ea063..3303d757 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -8,7 +8,6 @@ from kiota_abstractions.serialization import ParseNodeFactoryRegistry from kiota_abstractions.serialization import SerializationWriter -from kiota_serialization_json.json_parse_node import JsonParseNode from .batch_response_item import BatchResponseItem T = TypeVar('T', bound='Parsable') From cb26977f7fce052fc936a6cb40409f36a7f109f6 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:14:21 +0300 Subject: [PATCH 27/68] clean up request item --- .../requests/batch_request_item.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index 90d168b1..c6d2d5e6 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -1,7 +1,7 @@ import re import json from uuid import uuid4 -from typing import List, Optional, Dict, Callable, Union, Any +from typing import List, Optional, Dict, Union, Any from io import BytesIO import base64 import urllib.request @@ -33,7 +33,8 @@ def __init__( Args: request_information (RequestInformation): The request information. id (str, optional): The ID of the request item. Defaults to "". - depends_on (Optional[List[Union[str, BatchRequestItem]], optional): The IDs of the requests that this request depends on. Defaults to None. + depends_on (Optional[List[Union[str, BatchRequestItem]], optional): + The IDs of the requests that this request depends on. Defaults to None. """ if request_information is None or not request_information.http_method: raise ValueError("HTTP method cannot be Null/Empty") @@ -57,7 +58,8 @@ def create_with_urllib_request( Args: request (urllib.request.Request): The urllib request. id (str, optional): The ID of the request item. Defaults to "". - depends_on (Optional[List[str]], optional): The IDs of the requests that this request depends on. Defaults to None. + depends_on (Optional[List[str]], optional): The IDs of + the requests that this request depends on. Defaults to None. Returns: BatchRequestItem: A new instance of the BatchRequestItem class. """ @@ -74,7 +76,8 @@ def set_depends_on(self, requests: Optional[List[Union[str, 'BatchRequestItem']] """ Sets the IDs of the requests that this request depends on. Args: - requests (Optional[List[Union[str, BatchRequestItem]]): The IDs of the requests that this request depends on. + requests (Optional[List[Union[str, BatchRequestItem]]): The + IDs of the requests that this request depends on. """ if requests: for request in requests: @@ -101,7 +104,8 @@ def set_url(self, url: str) -> None: relative_url = re.sub(self.ME_TOKEN_REGEX, '/me', relative_url, 1) if not relative_url: raise ValueError( - f"Error occurred during regex replacement of '/users/me-token-to-replace' in URL string: {url}" + f"""Error occurred during regex replacement + of '/users/me-token-to-replace' in URL string: {url}""" ) self.url = relative_url @@ -210,7 +214,9 @@ def get_field_deserializers(self) -> Dict[str, Any]: """ Gets the deserialization information for this object. Returns: - Dict[str, Any]: The deserialization information for this object where each entry is a property key with its deserialization callback. + Dict[str, Any]: The deserialization information for + this object where each entry is a property key with its + deserialization callback. """ return { "id": self._id, From 54d7b7dbe5d2e02ad135b081019a256cf9f59582 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:20:46 +0300 Subject: [PATCH 28/68] clean up request builder --- src/msgraph_core/requests/batch_request_builder.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index fb0c74e9..fac1415b 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,19 +1,14 @@ -from typing import Type, TypeVar, Dict, Optional, Any -import asyncio +from typing import TypeVar, Dict import json from kiota_abstractions.request_adapter import RequestAdapter -from kiota_abstractions.serialization import ParsableFactory -from kiota_abstractions.serialization import Parsable from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.method import Method from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.api_error import APIError from .batch_request_content import BatchRequestContent -from .batch_request_content_collection import BatchRequestContentCollection from .batch_response_content import BatchResponseContent -from .batch_response_content_collection import BatchResponseContentCollection T = TypeVar('T', bound='Parsable') @@ -27,7 +22,7 @@ def __init__(self, request_adapter: RequestAdapter): if request_adapter is None: raise ValueError("request_adapter cannot be Null.") self._request_adapter = request_adapter - self.url_template = "{}/$batch".format(self._request_adapter.base_url) + self.url_template = "f{self._request_adapter.base_url}/$batch" async def post_content( self, @@ -38,7 +33,8 @@ async def post_content( Args: batch_request_content (BatchRequestContent): The batch request content. - error_map (Optional[Dict[str, ParsableFactory[Parsable]]]): Error mappings for response handling. + error_map (Optional[Dict[str, ParsableFactory[Parsable]]]): + Error mappings for response handling. Returns: BatchResponseContent: The batch response content. @@ -49,7 +45,6 @@ async def post_content( content = json.loads(request_info.content.decode("utf-8")) json_body = json.dumps(content) request_info.content = json_body - parsable_factory = BatchResponseContent() error_map: Dict[str, int] = {} response = None try: From 6ad12479f4729c657c58483a118e18ed42d8f43e Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:22:41 +0300 Subject: [PATCH 29/68] clean up request builder --- src/msgraph_core/requests/batch_request_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index fac1415b..56f2a3ce 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -4,6 +4,7 @@ from kiota_abstractions.request_adapter import RequestAdapter from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.method import Method +from kiota_abstractions.serialization import Parsable from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.api_error import APIError From 35befa4494bef638d910db55e88ff40bf5fc8807 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:27:07 +0300 Subject: [PATCH 30/68] clean up response content --- src/msgraph_core/requests/batch_response_content.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 3303d757..ecb5e9e5 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -1,10 +1,9 @@ -from typing import Optional, Dict, List, Any, Type, TypeVar, Callable +from typing import Optional, Dict, List, Type, TypeVar, Callable from io import BytesIO import base64 from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import ParseNode -from kiota_abstractions.serialization import ParseNodeFactory from kiota_abstractions.serialization import ParseNodeFactoryRegistry from kiota_abstractions.serialization import SerializationWriter @@ -18,7 +17,8 @@ class BatchResponseContent(Parsable): def __init__(self) -> None: """ Initializes a new instance of the BatchResponseContent class. - BatchResponseContent is a collection of BatchResponseItem items, each with a unique request ID. + BatchResponseContent is a collection of BatchResponseItem items, + each with a unique request ID. """ self._responses: Optional[List['BatchResponseItem']] = [] From e010943f7ad214de5e13fe9784d21c3c141441f2 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:28:57 +0300 Subject: [PATCH 31/68] clean up request collection --- src/msgraph_core/requests/batch_request_content_collection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index 07aace87..c0726dce 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -13,7 +13,8 @@ def __init__(self, batch_request_limit: int = 20): """ Initializes a new instance of the BatchRequestContentCollection class. Args: - batch_request_limit (int, optional): The maximum number of requests in a batch. Defaults to 20. + batch_request_limit (int, optional): The maximum + number of requests in a batch. Defaults to 20. """ self.batch_request_limit = batch_request_limit or BatchRequestContent.MAX_REQUESTS From 598f0b87f0b711f1480224638bf3580e7bc4846c Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:40:28 +0300 Subject: [PATCH 32/68] clean up response collection --- .../requests/batch_response_content_collection.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index 5a890f96..88b1906a 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -1,9 +1,7 @@ -from typing import List, Optional, Dict, Callable +from typing import Optional, Dict, Callable from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import ParseNode -from kiota_abstractions.serialization import ParseNodeFactory -from kiota_abstractions.serialization import ParseNodeFactoryRegistry from kiota_abstractions.serialization import SerializationWriter from .batch_response_content import BatchResponseContent @@ -33,7 +31,7 @@ def add_response(self, content: Optional[BatchResponseItem] = None) -> None: if content is None: return for item in content: - self._responses.add_response(item) + self._responses.response = item async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseItem]: """ @@ -81,7 +79,9 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: return { 'responses': lambda n: setattr( - self, "_responses", n.get_collection_of_object_values(BatchResponseItem.create) + self, "_responses", + n. + get_collection_of_object_values(BatchResponseItem.create_from_discriminator_value) ) } @@ -91,4 +91,4 @@ def serialize(self, writer: SerializationWriter) -> None: :param writer: The writer to write to. :type writer: SerializationWriter """ - writer.write_collection_of_object_values('responses', self._responses.values()) + writer.write_collection_of_object_values('responses', self._responses) From 83255df87b86b5b819fc4bedede84ed79ba8146a Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:43:28 +0300 Subject: [PATCH 33/68] clean up response collection --- src/msgraph_core/requests/batch_response_content_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index 88b1906a..b53dd25f 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -31,7 +31,7 @@ def add_response(self, content: Optional[BatchResponseItem] = None) -> None: if content is None: return for item in content: - self._responses.response = item + self._responses.responses = content async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseItem]: """ From 63a1048bb16f672c9538ab7eae992db771cb72bf Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 17:49:28 +0300 Subject: [PATCH 34/68] clean up code --- src/msgraph_core/requests/batch_request_item.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index c6d2d5e6..ed53f610 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -104,10 +104,9 @@ def set_url(self, url: str) -> None: relative_url = re.sub(self.ME_TOKEN_REGEX, '/me', relative_url, 1) if not relative_url: raise ValueError( - f"""Error occurred during regex replacement + f"""Error occurred during regex replacement of '/users/me-token-to-replace' in URL string: {url}""" ) - self.url = relative_url if url_parts.query: self.url += f"?{url_parts.query}" From a4c30bd2445106f38fe8e895d968a695bdf13ac3 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 18:11:01 +0300 Subject: [PATCH 35/68] write unit tests for request_item --- tests/requests/__init__.py | 0 tests/requests/test_batch_request_item.py | 77 +++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tests/requests/__init__.py create mode 100644 tests/requests/test_batch_request_item.py diff --git a/tests/requests/__init__.py b/tests/requests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/requests/test_batch_request_item.py b/tests/requests/test_batch_request_item.py new file mode 100644 index 00000000..f70c40f3 --- /dev/null +++ b/tests/requests/test_batch_request_item.py @@ -0,0 +1,77 @@ +import pytest +from unittest.mock import Mock +from urllib.request import Request +from kiota_abstractions.request_information import RequestInformation +from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders +from msgraph_core.requests.batch_request_item import BatchRequestItem, StreamInterface +from kiota_abstractions.serialization import SerializationWriter + +base_url = "https://graph.microsoft.com/v1.0/me" + + +@pytest.fixture +def request_info(): + request_info = RequestInformation() + request_info.http_method = "GET" + request_info.url = "f{base_url}/me" + request_info.headers = RequestHeaders() + request_info.content = StreamInterface(b'{"key": "value"}') + return request_info + + +@pytest.fixture +def batch_request_item(request_info): + return BatchRequestItem(request_information=request_info) + + +def test_initialization(batch_request_item, request_info): + assert batch_request_item.method == "GET" + assert batch_request_item.url == "f{base_url}/me" + assert batch_request_item.body.read() == b'{"key": "value"}' + + +def test_create_with_urllib_request(): + urllib_request = Request("https://graph.microsoft.com/v1.0/me", method="POST") + urllib_request.add_header("Content-Type", "application/json") + urllib_request.data = b'{"key": "value"}' + batch_request_item = BatchRequestItem.create_with_urllib_request(urllib_request) + assert batch_request_item.method == "POST" + assert batch_request_item.url == "https://graph.microsoft.com/v1.0/me" + assert batch_request_item.body == b'{"key": "value"}' + + +def test_set_depends_on(batch_request_item): + batch_request_item.set_depends_on(["request1", "request2"]) + assert batch_request_item.depends_on == ["request1", "request2"] + + +def test_set_url(batch_request_item): + batch_request_item.set_url("https://graph.microsoft.com/v1.0/me") + assert batch_request_item.url == "/v1.0/me" + + +def test_id_property(batch_request_item): + batch_request_item.id = "new_id" + assert batch_request_item.id == "new_id" + + +def test_headers_property(batch_request_item): + new_headers = {"Authorization": "Bearer token"} + batch_request_item.headers = new_headers + assert batch_request_item.headers["Authorization"] == "Bearer token" + + +def test_body_property(batch_request_item): + new_body = StreamInterface(b'{"new_key": "new_value"}') + batch_request_item.body = new_body + assert batch_request_item.body.read() == b'{"new_key": "new_value"}' + + +def test_method_property(batch_request_item): + batch_request_item.method = "POST" + assert batch_request_item.method == "POST" + + +def test_depends_on_property(batch_request_item): + batch_request_item.set_depends_on(["request1", "request2"]) + assert batch_request_item.depends_on == ["request1", "request2"] From ed9030414d152e4b4c9973e8689c3cc9d3efff86 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 18:35:08 +0300 Subject: [PATCH 36/68] added tests for request content --- tests/requests/test_batch_request_content.py | 119 +++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/requests/test_batch_request_content.py diff --git a/tests/requests/test_batch_request_content.py b/tests/requests/test_batch_request_content.py new file mode 100644 index 00000000..504f8c98 --- /dev/null +++ b/tests/requests/test_batch_request_content.py @@ -0,0 +1,119 @@ +import pytest +from unittest.mock import Mock +from urllib.request import Request +from kiota_abstractions.request_information import RequestInformation +from kiota_abstractions.serialization import SerializationWriter +from msgraph_core.requests.batch_request_item import BatchRequestItem +from msgraph_core.requests.batch_request_content import BatchRequestContent +from kiota_abstractions.headers_collection import HeadersCollection as RequestHeaders +from msgraph_core.requests.batch_request_item import BatchRequestItem, StreamInterface + + +@pytest.fixture +def request_info1(): + request_info = RequestInformation() + request_info.http_method = "GET" + request_info.url = "https://graph.microsoft.com/v1.0/me" + request_info.headers = RequestHeaders() + request_info.headers.add("Content-Type", "application/json") + request_info.content = StreamInterface(b'{"key": "value"}') + return request_info + + +@pytest.fixture +def request_info2(): + request_info = RequestInformation() + request_info.http_method = "POST" + request_info.url = "https://graph.microsoft.com/v1.0/users" + request_info.headers = RequestHeaders() + request_info.headers.add("Content-Type", "application/json") + request_info.content = StreamInterface(b'{"key": "value"}') + return request_info + + +@pytest.fixture +def batch_request_item1(request_info1): + return BatchRequestItem(request_information=request_info1) + + +@pytest.fixture +def batch_request_item2(request_info2): + return BatchRequestItem(request_information=request_info2) + + +@pytest.fixture +def batch_request_content(batch_request_item1, batch_request_item2): + return BatchRequestContent(requests=[batch_request_item1, batch_request_item2]) + + +def test_initialization(batch_request_content, batch_request_item1, batch_request_item2): + assert len(batch_request_content.requests) == 2 + assert batch_request_content.requests[0] == batch_request_item1 + assert batch_request_content.requests[1] == batch_request_item2 + + +def test_requests_property(batch_request_content, batch_request_item1, batch_request_item2): + new_request_item = batch_request_item1 + batch_request_content.requests = [batch_request_item1, batch_request_item2, new_request_item] + assert len(batch_request_content.requests) == 5 + assert batch_request_content.requests[2] == new_request_item + + +def test_add_request(batch_request_content, batch_request_item1): + new_request_item = request_info1 + new_request_item.id = "new_id" + batch_request_content.add_request(new_request_item) + assert len(batch_request_content.requests) == 3 + assert batch_request_content.requests[-1] == new_request_item + + +def test_add_request_information(batch_request_content): + new_request_info = RequestInformation() + new_request_info.http_method = "DELETE" + new_request_info.url = "https://graph.microsoft.com/v1.0/groups" + batch_request_content.add_request_information(new_request_info) + assert len(batch_request_content.requests) == 3 + + +def test_add_urllib_request(batch_request_content): + urllib_request = Request("https://graph.microsoft.com/v1.0/me", method="PATCH") + urllib_request.add_header("Content-Type", "application/json") + urllib_request.data = b'{"key": "value"}' + batch_request_content.add_urllib_request(urllib_request) + assert len(batch_request_content.requests) == 3 + assert batch_request_content.requests[-1].method == "PATCH" + + +def test_remove(batch_request_content, batch_request_item1): + batch_request_content.remove(batch_request_item1.id) + assert len(batch_request_content.requests) == 1 + + +def test_remove_batch_request_item(batch_request_content, batch_request_item1): + batch_request_content.remove_batch_request_item(batch_request_item1) + assert len(batch_request_content.requests) == 1 + + +def test_finalize(batch_request_content): + finalized_requests = batch_request_content.finalize() + assert batch_request_content.is_finalized + assert finalized_requests == batch_request_content.requests + + +def test_create_from_discriminator_value(): + parse_node = Mock() + batch_request_content = BatchRequestContent.create_from_discriminator_value(parse_node) + assert isinstance(batch_request_content, BatchRequestContent) + + +def test_get_field_deserializers(batch_request_content): + deserializers = batch_request_content.get_field_deserializers() + assert isinstance(deserializers, dict) + + +def test_serialize(batch_request_content): + writer = Mock(spec=SerializationWriter) + batch_request_content.serialize(writer) + writer.write_collection_of_object_values.assert_called_once_with( + "requests", batch_request_content.requests + ) From effd3180cae801fd779a25ed3b4dab0af7783b92 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 19:00:53 +0300 Subject: [PATCH 37/68] write tests for response models --- .../requests/batch_response_item.py | 6 +- tests/requests/test_batch_response_content.py | 71 ++++++++++++++++ tests/requests/test_batch_response_item.py | 84 +++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 tests/requests/test_batch_response_content.py create mode 100644 tests/requests/test_batch_response_item.py diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 01e7c768..691eae19 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -65,7 +65,7 @@ def status(self) -> Optional[int]: :return: The status code of the response :rtype: Optional[int] """ - return self._status_code + return self._status @status.setter def status(self, status_code: Optional[int]) -> None: @@ -74,7 +74,7 @@ def status(self, status_code: Optional[int]) -> None: :param status_code: The status code of the response :type status_code: Optional[int] """ - self._status_code = status_code + self._status = status_code @property def headers(self) -> Optional[Dict[str, str]]: @@ -157,6 +157,6 @@ def serialize(self, writer: SerializationWriter) -> None: """ writer.write_str_value('id', self._id) writer.write_str_value('atomicity_group', self._atomicity_group) - writer.write_int_value('status_code', self._status_code) + writer.write_int_value('status', self._status) writer.write_collection_of_primitive_values('headers', self._headers) writer.write_bytes_value('body', self._body) diff --git a/tests/requests/test_batch_response_content.py b/tests/requests/test_batch_response_content.py new file mode 100644 index 00000000..4c416dca --- /dev/null +++ b/tests/requests/test_batch_response_content.py @@ -0,0 +1,71 @@ +import pytest +from unittest.mock import Mock +from io import BytesIO +from kiota_abstractions.serialization import ParseNode, SerializationWriter, Parsable, ParseNodeFactoryRegistry +from msgraph_core.requests.batch_response_item import BatchResponseItem +from msgraph_core.requests.batch_response_content import BatchResponseContent + + +@pytest.fixture +def batch_response_content(): + return BatchResponseContent() + + +def test_initialization(batch_response_content): + assert batch_response_content.responses == [] + + +def test_responses_property(batch_response_content): + response_item = Mock(spec=BatchResponseItem) + batch_response_content.responses = [response_item] + assert batch_response_content.responses == [response_item] + + +def test_response_method(batch_response_content): + response_item = Mock(spec=BatchResponseItem) + response_item.request_id = "12345" + batch_response_content.responses = [response_item] + assert batch_response_content.response("12345") == response_item + + +def test_get_response_by_id_method(batch_response_content): + response_item = Mock(spec=BatchResponseItem) + response_item.request_id = "12345" + batch_response_content.responses = [response_item] + assert batch_response_content.get_response_by_id("12345") == response_item + + +def test_response_body_method(batch_response_content): + response_item = Mock(spec=BatchResponseItem) + response_item.request_id = "12345" + response_item.content_type = "application/json" + response_item.body = BytesIO(b'{"key": "value"}') + batch_response_content.responses = [response_item] + + parse_node = Mock(spec=ParseNode) + parse_node.get_object_value.return_value = {"key": "value"} + registry = Mock(spec=ParseNodeFactoryRegistry) + registry.get_root_parse_node.return_value = parse_node + + with pytest.raises(ValueError): + batch_response_content.response_body("12345", dict) + + +def test_get_field_deserializers(batch_response_content): + deserializers = batch_response_content.get_field_deserializers() + assert isinstance(deserializers, dict) + assert "responses" in deserializers + + +def test_serialize(batch_response_content): + writer = Mock(spec=SerializationWriter) + response_item = Mock(spec=BatchResponseItem) + batch_response_content.responses = [response_item] + batch_response_content.serialize(writer) + writer.write_collection_of_object_values.assert_called_once_with('responses', [response_item]) + + +def test_create_from_discriminator_value(): + parse_node = Mock(spec=ParseNode) + batch_response_content = BatchResponseContent.create_from_discriminator_value(parse_node) + assert isinstance(batch_response_content, BatchResponseContent) diff --git a/tests/requests/test_batch_response_item.py b/tests/requests/test_batch_response_item.py new file mode 100644 index 00000000..5d34d6b4 --- /dev/null +++ b/tests/requests/test_batch_response_item.py @@ -0,0 +1,84 @@ +import pytest +from io import BytesIO + +from kiota_abstractions.serialization import ParseNode, SerializationWriter +from unittest.mock import Mock + +from msgraph_core.requests.batch_response_item import BatchResponseItem, StreamInterface + + +@pytest.fixture +def batch_response_item(): + return BatchResponseItem() + + +def test_initialization(batch_response_item): + assert batch_response_item.id is None + assert batch_response_item.atomicity_group is None + assert batch_response_item.status is None + assert batch_response_item.headers == {} + assert batch_response_item.body is None + + +def test_id_property(batch_response_item): + batch_response_item.id = "12345" + assert batch_response_item.id == "12345" + + +def test_atomicity_group_property(batch_response_item): + batch_response_item.atomicity_group = "group1" + assert batch_response_item.atomicity_group == "group1" + + +def test_status_property(batch_response_item): + batch_response_item.status = 200 + assert batch_response_item.status == 200 + + +def test_headers_property(batch_response_item): + headers = {"Content-Type": "application/json"} + batch_response_item.headers = headers + assert batch_response_item.headers == headers + + +def test_body_property(batch_response_item): + body = StreamInterface(b"response body") + batch_response_item.body = body + assert batch_response_item.body == body + + +def test_content_type_property(batch_response_item): + headers = {"Content-Type": "application/json"} + batch_response_item.headers = headers + assert batch_response_item.content_type == "application/json" + + +def test_create_from_discriminator_value(): + parse_node = Mock(spec=ParseNode) + batch_response_item = BatchResponseItem.create_from_discriminator_value(parse_node) + assert isinstance(batch_response_item, BatchResponseItem) + + +def test_get_field_deserializers(batch_response_item): + deserializers = batch_response_item.get_field_deserializers() + assert isinstance(deserializers, dict) + assert "id" in deserializers + assert "status" in deserializers + assert "headers" in deserializers + assert "body" in deserializers + + +def test_serialize(batch_response_item): + writer = Mock(spec=SerializationWriter) + batch_response_item.id = "12345" + batch_response_item.atomicity_group = "group1" + batch_response_item.status = 200 + batch_response_item.headers = {"Content-Type": "application/json"} + batch_response_item.body = StreamInterface(b"response body") + batch_response_item.serialize(writer) + writer.write_str_value.assert_any_call('id', "12345") + writer.write_str_value.assert_any_call('atomicity_group', "group1") + writer.write_int_value.assert_any_call('status', 200) + writer.write_collection_of_primitive_values.assert_any_call( + 'headers', {"Content-Type": "application/json"} + ) From 9fc4e10250efb9c860089ae45492511add30b66c Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 23:11:53 +0300 Subject: [PATCH 38/68] deserialize body and headers --- src/msgraph_core/requests/batch_request_builder.py | 2 +- src/msgraph_core/requests/batch_response_item.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 56f2a3ce..2cd22620 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -23,7 +23,7 @@ def __init__(self, request_adapter: RequestAdapter): if request_adapter is None: raise ValueError("request_adapter cannot be Null.") self._request_adapter = request_adapter - self.url_template = "f{self._request_adapter.base_url}/$batch" + self.url_template = "{}/$batch".format(self._request_adapter.base_url) async def post_content( self, diff --git a/src/msgraph_core/requests/batch_response_item.py b/src/msgraph_core/requests/batch_response_item.py index 691eae19..91c39881 100644 --- a/src/msgraph_core/requests/batch_response_item.py +++ b/src/msgraph_core/requests/batch_response_item.py @@ -146,9 +146,8 @@ def get_field_deserializers(self) -> Dict[str, Any]: return { "id": lambda x: setattr(self, "id", x.get_str_value()), "status": lambda x: setattr(self, "status", x.get_int_value()), - "headers": - lambda x: setattr(self, "headers", x.get_collection_of_primitive_values(str)), - "body": lambda x: setattr(self, "body", x.get_collection_of_primitive_values(str)), + "headers": lambda x: setattr(self, "headers", x.try_get_anything(x._json_node)), + "body": lambda x: setattr(self, "body", x.get_bytes_value()), } def serialize(self, writer: SerializationWriter) -> None: From d54af6aa6cddb7cb5d19b0c88a9ca425807c228f Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 23:15:00 +0300 Subject: [PATCH 39/68] clean up code --- src/msgraph_core/requests/batch_request_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 2cd22620..7c144e2c 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -23,7 +23,7 @@ def __init__(self, request_adapter: RequestAdapter): if request_adapter is None: raise ValueError("request_adapter cannot be Null.") self._request_adapter = request_adapter - self.url_template = "{}/$batch".format(self._request_adapter.base_url) + self.url_template = f"{self._request_adapter.base_url}/$batch" async def post_content( self, From 2d2c6719b0f5c180d7ddd3f212b0e3191a4be33d Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 23:44:53 +0300 Subject: [PATCH 40/68] code clean up --- .../requests/batch_request_builder.py | 15 ++++++++++----- src/msgraph_core/requests/batch_request_item.py | 2 -- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 7c144e2c..4c4ba3e4 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,5 +1,6 @@ from typing import TypeVar, Dict import json +import logging from kiota_abstractions.request_adapter import RequestAdapter from kiota_abstractions.request_information import RequestInformation @@ -13,6 +14,8 @@ T = TypeVar('T', bound='Parsable') +APPLICATION_JSON = "application/json" + class BatchRequestBuilder: """ @@ -28,13 +31,14 @@ def __init__(self, request_adapter: RequestAdapter): async def post_content( self, batch_request_content: BatchRequestContent, + error_map: Dict[str, int] = {} ) -> BatchResponseContent: """ Sends a batch request and returns the batch response content. Args: batch_request_content (BatchRequestContent): The batch request content. - error_map (Optional[Dict[str, ParsableFactory[Parsable]]]): + error_map: Dict[str, int] = {}: Error mappings for response handling. Returns: @@ -53,10 +57,10 @@ async def post_content( request_info, BatchResponseContent, error_map ) except APIError as e: - print(f"API Error: {e}") + logging.error(f"API Error: {e}") + raise e if response is None: raise ValueError("Failed to get a valid response from the API.") - print(f"Response before processing: {response}") return response async def to_post_request_information( @@ -77,13 +81,14 @@ async def to_post_request_information( request_info = RequestInformation() request_info.http_method = Method.POST request_info.url_template = self.url_template + requests_dict = [item.get_field_deserializers() for item in batch_request_content.requests] request_info.content = json.dumps({"requests": requests_dict}).encode("utf-8") request_info.headers = HeadersCollection() - request_info.headers.try_add("Content-Type", "application/json") + request_info.headers.try_add("Content-Type", APPLICATION_JSON) request_info.set_content_from_parsable( - self._request_adapter, "application/json", batch_request_content + self._request_adapter, APPLICATION_JSON, batch_request_content ) return request_info diff --git a/src/msgraph_core/requests/batch_request_item.py b/src/msgraph_core/requests/batch_request_item.py index ed53f610..e5f46e8b 100644 --- a/src/msgraph_core/requests/batch_request_item.py +++ b/src/msgraph_core/requests/batch_request_item.py @@ -239,10 +239,8 @@ def serialize(self, writer: SerializationWriter) -> None: headers = {key: ", ".join(val) for key, val in self._headers.items()} writer.write_collection_of_object_values('headers', headers) if self._body: - print(f"Body: {self._body}") json_object = json.loads(self._body) is_json_string = json_object and isinstance(json_object, dict) - # self.body.seek(0) writer.write_collection_of_object_values( 'body', json_object if is_json_string else base64.b64encode(self._body).decode('utf-8') From 6e6f54225978f34d45750035c91c2483082957e4 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 9 Sep 2024 23:47:03 +0300 Subject: [PATCH 41/68] code clean up --- src/msgraph_core/requests/batch_request_builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 4c4ba3e4..3361de05 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -31,7 +31,6 @@ def __init__(self, request_adapter: RequestAdapter): async def post_content( self, batch_request_content: BatchRequestContent, - error_map: Dict[str, int] = {} ) -> BatchResponseContent: """ Sends a batch request and returns the batch response content. From 37c18ce0deba594fb8e252af6d5ade1579b0db5c Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 10 Sep 2024 14:58:23 +0300 Subject: [PATCH 42/68] make error map an optional param --- src/msgraph_core/requests/batch_request_builder.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 3361de05..5cdccefd 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Dict +from typing import TypeVar, Dict, Optional import json import logging @@ -22,15 +22,17 @@ class BatchRequestBuilder: Provides operations to call the batch method. """ - def __init__(self, request_adapter: RequestAdapter): + def __init__(self, request_adapter: RequestAdapter, error_map: Optional[Dict[str, int]] = None): if request_adapter is None: raise ValueError("request_adapter cannot be Null.") self._request_adapter = request_adapter self.url_template = f"{self._request_adapter.base_url}/$batch" + self.error_map = error_map or {} async def post_content( self, batch_request_content: BatchRequestContent, + error_map: Optional[Dict[str, int]] = None, ) -> BatchResponseContent: """ Sends a batch request and returns the batch response content. @@ -49,7 +51,7 @@ async def post_content( content = json.loads(request_info.content.decode("utf-8")) json_body = json.dumps(content) request_info.content = json_body - error_map: Dict[str, int] = {} + error_map = error_map or self.error_map response = None try: response = await self._request_adapter.send_async( From 0807bbdedee5cfbb0a9042caff665febae4f4dde Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 10 Sep 2024 15:03:09 +0300 Subject: [PATCH 43/68] update success status codes to a list --- src/msgraph_core/requests/batch_request_content_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index c0726dce..3692cfdd 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -63,7 +63,7 @@ def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: batch_with_failed_responses: Optional[BatchRequestContent] = BatchRequestContent() for batch in self.batches: for request in batch.requests: - if request.status_code != 200: + if request.status_code not in [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]: if batch_with_failed_responses is not None: batch_with_failed_responses.add_request(request) else: From 2bfbba48a3586a5e7e73233b4ab195ea4c036fc6 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 10 Sep 2024 15:06:44 +0300 Subject: [PATCH 44/68] rename post_content to post --- src/msgraph_core/requests/batch_request_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 5cdccefd..eb1c26cb 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -29,7 +29,7 @@ def __init__(self, request_adapter: RequestAdapter, error_map: Optional[Dict[str self.url_template = f"{self._request_adapter.base_url}/$batch" self.error_map = error_map or {} - async def post_content( + async def post( self, batch_request_content: BatchRequestContent, error_map: Optional[Dict[str, int]] = None, From 3015f19b49d53d766e8a43c7b9c9011921c13cb7 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 10 Sep 2024 19:15:46 +0300 Subject: [PATCH 45/68] start post collection --- .../requests/batch_request_builder.py | 152 +++++++++++++++++- .../requests/batch_request_content.py | 2 +- .../batch_request_content_collection.py | 21 ++- .../batch_response_content_collection.py | 38 ++--- 4 files changed, 173 insertions(+), 40 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index eb1c26cb..0d23de93 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Dict, Optional +from typing import TypeVar, Dict, Optional, Union, List import json import logging @@ -10,7 +10,9 @@ from kiota_abstractions.api_error import APIError from .batch_request_content import BatchRequestContent +from .batch_request_content_collection import BatchRequestContentCollection from .batch_response_content import BatchResponseContent +from .batch_response_content_collection import BatchResponseContentCollection T = TypeVar('T', bound='Parsable') @@ -31,7 +33,7 @@ def __init__(self, request_adapter: RequestAdapter, error_map: Optional[Dict[str async def post( self, - batch_request_content: BatchRequestContent, + batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], error_map: Optional[Dict[str, int]] = None, ) -> BatchResponseContent: """ @@ -47,12 +49,74 @@ async def post( """ if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") - request_info = await self.to_post_request_information(batch_request_content) + if isinstance(batch_request_content, BatchRequestContent): + request_info = await self.to_post_request_information(batch_request_content) + content = json.loads(request_info.content.decode("utf-8")) + json_body = json.dumps(content) + request_info.content = json_body + error_map = error_map or self.error_map + response = None + try: + response = await self._request_adapter.send_async( + request_info, BatchResponseContent, error_map + ) + except APIError as e: + logging.error(f"API Error: {e}") + raise e + if response is None: + raise ValueError("Failed to get a valid response from the API.") + return response + if isinstance(batch_request_content, BatchRequestContentCollection): + request_info = await self.to_post_request_information_from_collection( + batch_request_content + ) + + content = json.loads(request_info.content.decode("utf-8")) + json_body = json.dumps(content) + request_info.content = json_body + error_map = error_map or self.error_map + response = None + try: + response = await self._request_adapter.send_async( + request_info, BatchResponseContent, error_map + ) + except APIError as e: + logging.error(f"API Error: {e}") + raise e + if response is None: + raise ValueError("Failed to get a valid response from the API.") + return response + + async def post_content( + self, + batch_request_content: BatchRequestContentCollection, + error_map: Optional[Dict[str, int]] = None, + ) -> BatchResponseContentCollection: + if batch_request_content is None: + raise ValueError("batch_request_content cannot be Null.") + + # Determine the type of batch_request_content and call the appropriate method + if isinstance(batch_request_content, BatchRequestContent): + request_info = await self.to_post_request_information(batch_request_content) + elif isinstance(batch_request_content, BatchRequestContentCollection): + request_info = await self.to_post_request_information_from_collection( + batch_request_content + ) + batch_responses = await self.post_batch_collection(batch_request_content) + return batch_responses + else: + raise ValueError("Invalid type for batch_request_content.") + + print(f"request info to be posted {request_info}") + + # Decode and re-encode the content to ensure it is in the correct format content = json.loads(request_info.content.decode("utf-8")) json_body = json.dumps(content) - request_info.content = json_body + request_info.content = json_body.encode("utf-8") + error_map = error_map or self.error_map response = None + try: response = await self._request_adapter.send_async( request_info, BatchResponseContent, error_map @@ -60,13 +124,46 @@ async def post( except APIError as e: logging.error(f"API Error: {e}") raise e + if response is None: raise ValueError("Failed to get a valid response from the API.") + return response - async def to_post_request_information( + async def post_batch_collection( self, - batch_request_content: BatchRequestContent, + batch_request_content_collection: BatchRequestContentCollection, + error_map: Optional[Dict[str, int]] = None, + ) -> BatchResponseContentCollection: + """ + Sends a collection of batch requests and returns a collection of batch response contents. + + Args: + batch_request_content_collection (BatchRequestContentCollection): The collection of batch request contents. + error_map: Dict[str, int] = {}: + Error mappings for response handling. + + Returns: + BatchResponseContentCollection: The collection of batch response contents. + """ + if batch_request_content_collection is None: + raise ValueError("batch_request_content_collection cannot be Null.") + + batch_responses = BatchResponseContentCollection() + + for batch_request_content in batch_request_content_collection.batches: + request_info = await self.to_post_request_information(batch_request_content) + print(f"request_info content before call {request_info.content}") + # response = await self._request_adapter.send_async( + # request_info, BatchResponseContent, error_map + # ) + # = await self.post_content(batch_request_content, error_map) + # batch_responses.add_response(batch_request_content.requests.keys(), response) + + return batch_responses + + async def to_post_request_information( + self, batch_request_content: BatchRequestContent ) -> RequestInformation: """ Creates request information for a batch POST request. @@ -79,12 +176,51 @@ async def to_post_request_information( """ if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") + if isinstance(batch_request_content, BatchRequestContent): + request_info = RequestInformation() + request_info.http_method = Method.POST + request_info.url_template = self.url_template + + requests_dict = [ + item.get_field_deserializers() for item in batch_request_content.requests + ] + request_info.content = json.dumps({"requests": requests_dict}).encode("utf-8") + + request_info.headers = HeadersCollection() + request_info.headers.try_add("Content-Type", APPLICATION_JSON) + request_info.set_content_from_parsable( + self._request_adapter, APPLICATION_JSON, batch_request_content + ) + + return request_info + + async def to_post_request_information_from_collection( + self, batch_request_content: BatchRequestContentCollection + ) -> RequestInformation: + """ + Creates request information for a batch POST request from a collection. + + Args: + batch_request_content (BatchRequestContentCollection): The batch request content collection. + + Returns: + RequestInformation: The request information. + """ + if batch_request_content is None: + raise ValueError("batch_request_content cannot be Null.") + request_info = RequestInformation() request_info.http_method = Method.POST request_info.url_template = self.url_template - requests_dict = [item.get_field_deserializers() for item in batch_request_content.requests] - request_info.content = json.dumps({"requests": requests_dict}).encode("utf-8") + all_requests = [] + for batch_content in batch_request_content.batches: + print(f"batch_content {batch_content}") + requests_dict = [item.get_field_deserializers() for item in batch_content.requests] + all_requests.extend(requests_dict) + + request_info.content = json.dumps({"requests": all_requests}).encode("utf-8") + print(f"All requests {request_info.content}") request_info.headers = HeadersCollection() request_info.headers.try_add("Content-Type", APPLICATION_JSON) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 2518a871..840a3f65 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -13,7 +13,7 @@ class BatchRequestContent(Parsable): Provides operations to call the batch method. """ - MAX_REQUESTS = 20 + MAX_REQUESTS = 5 def __init__(self, requests: List[Union['BatchRequestItem', 'RequestInformation']] = []): """ diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index 3692cfdd..532131b2 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -1,6 +1,7 @@ from typing import List, Optional from kiota_abstractions.request_information import RequestInformation +from kiota_abstractions.serialization import SerializationWriter from .batch_request_content import BatchRequestContent from .batch_request_item import BatchRequestItem @@ -9,15 +10,13 @@ class BatchRequestContentCollection: """A collection of request content objects.""" - def __init__(self, batch_request_limit: int = 20): + def __init__(self) -> None: """ Initializes a new instance of the BatchRequestContentCollection class. Args: - batch_request_limit (int, optional): The maximum number of requests in a batch. Defaults to 20. """ - self.batch_request_limit = batch_request_limit or BatchRequestContent.MAX_REQUESTS self.batches: List[BatchRequestContent] = [] self.current_batch: BatchRequestContent = BatchRequestContent() @@ -76,7 +75,17 @@ def get_batch_requests_for_execution(self) -> List[BatchRequestContent]: Returns: List[BatchRequestContent]: The batch requests for execution. """ - if not self.current_batch.is_finalized: - self.current_batch.finalize() - self.batches.append(self.current_batch) + # if not self.current_batch.is_finalized: + # self.current_batch.finalize() + # self.batches.append(self.current_batch) return self.batches + + def serialize(self, writer: SerializationWriter) -> None: + """ + Serializes information the current object + Args: + writer: Serialization writer to use to serialize this model + """ + pass + # print(f"serializing {self.batches}") + # writer.write_collection_of_object_values("requests", self.batches) diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index b53dd25f..fefa9624 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -20,36 +20,24 @@ def __init__(self) -> None: body: Optional[StreamInterface] = None """ - self._responses: BatchResponseContent = BatchResponseContent() + self._responses = [] - def add_response(self, content: Optional[BatchResponseItem] = None) -> None: + def add_response(self, keys, response) -> None: """ - Add a response to the collection - :param content: The response to add to the collection - :type content: Optional[BatchResponseItem] + Adds a response to the collection. + Args: + keys: The keys of the response to add. + response: The response to add. """ - if content is None: - return - for item in content: - self._responses.responses = content + self._responses.append((keys, response)) - async def get_response_by_id(self, request_id: str) -> Optional[BatchResponseItem]: - """ - Get a response by its request ID from the collection - :param request_id: The request ID of the response to get - :type request_id: str - :return: The response with the specified request ID as a BatchResponseItem - :rtype: Optional[BatchResponseItem] + def get_responses(self): + """ + Gets the responses in the collection. + Returns: + List[Tuple[str, BatchResponseContent]]: The responses in the collection. """ - if not self._responses: - raise ValueError("No responses found in the collection") - if isinstance(self._responses, BatchResponseContent): - if self._responses.responses is None: - raise ValueError("No responses found in the collection") - for response in self._responses.responses: - if isinstance(response, BatchResponseItem) and response.id == request_id: - return response - raise TypeError("Invalid type: Collection must be of type BatchResponseContent") + return self._responses @property async def responses_status_codes(self) -> Dict[str, int]: From b871815b088c72ce4cfcd9dfe84eb352bf32a83f Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 10 Sep 2024 21:32:50 +0300 Subject: [PATCH 46/68] fix post batch collection --- src/msgraph_core/requests/batch_request_builder.py | 14 ++++---------- .../requests/batch_response_content_collection.py | 8 ++++---- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 0d23de93..77e6fbbd 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -107,8 +107,6 @@ async def post_content( else: raise ValueError("Invalid type for batch_request_content.") - print(f"request info to be posted {request_info}") - # Decode and re-encode the content to ensure it is in the correct format content = json.loads(request_info.content.decode("utf-8")) json_body = json.dumps(content) @@ -153,12 +151,10 @@ async def post_batch_collection( for batch_request_content in batch_request_content_collection.batches: request_info = await self.to_post_request_information(batch_request_content) - print(f"request_info content before call {request_info.content}") - # response = await self._request_adapter.send_async( - # request_info, BatchResponseContent, error_map - # ) - # = await self.post_content(batch_request_content, error_map) - # batch_responses.add_response(batch_request_content.requests.keys(), response) + response = await self._request_adapter.send_async( + request_info, BatchResponseContent, error_map + ) + batch_responses.add_response(response) return batch_responses @@ -215,12 +211,10 @@ async def to_post_request_information_from_collection( all_requests = [] for batch_content in batch_request_content.batches: - print(f"batch_content {batch_content}") requests_dict = [item.get_field_deserializers() for item in batch_content.requests] all_requests.extend(requests_dict) request_info.content = json.dumps({"requests": all_requests}).encode("utf-8") - print(f"All requests {request_info.content}") request_info.headers = HeadersCollection() request_info.headers.try_add("Content-Type", APPLICATION_JSON) diff --git a/src/msgraph_core/requests/batch_response_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py index fefa9624..e30ff30d 100644 --- a/src/msgraph_core/requests/batch_response_content_collection.py +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Callable +from typing import Optional, Dict, Callable, List from kiota_abstractions.serialization import Parsable from kiota_abstractions.serialization import ParseNode @@ -20,16 +20,16 @@ def __init__(self) -> None: body: Optional[StreamInterface] = None """ - self._responses = [] + self._responses: List[BatchResponseContent] = [] - def add_response(self, keys, response) -> None: + def add_response(self, response: BatchResponseContent) -> None: """ Adds a response to the collection. Args: keys: The keys of the response to add. response: The response to add. """ - self._responses.append((keys, response)) + self._responses.append(response) def get_responses(self): """ From cf35e47d03d837c3a6367e5212e35896c84bac45 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Tue, 10 Sep 2024 21:47:12 +0300 Subject: [PATCH 47/68] clean up post batch with collection --- .../requests/batch_request_builder.py | 98 +------------------ 1 file changed, 5 insertions(+), 93 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 77e6fbbd..61520522 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -61,73 +61,18 @@ async def post( request_info, BatchResponseContent, error_map ) except APIError as e: - logging.error(f"API Error: {e}") + logging.error("API Error: %s", e) raise e if response is None: raise ValueError("Failed to get a valid response from the API.") return response - if isinstance(batch_request_content, BatchRequestContentCollection): - request_info = await self.to_post_request_information_from_collection( - batch_request_content - ) - - content = json.loads(request_info.content.decode("utf-8")) - json_body = json.dumps(content) - request_info.content = json_body - error_map = error_map or self.error_map - response = None - try: - response = await self._request_adapter.send_async( - request_info, BatchResponseContent, error_map - ) - except APIError as e: - logging.error(f"API Error: {e}") - raise e - if response is None: - raise ValueError("Failed to get a valid response from the API.") - return response - - async def post_content( - self, - batch_request_content: BatchRequestContentCollection, - error_map: Optional[Dict[str, int]] = None, - ) -> BatchResponseContentCollection: - if batch_request_content is None: - raise ValueError("batch_request_content cannot be Null.") - - # Determine the type of batch_request_content and call the appropriate method - if isinstance(batch_request_content, BatchRequestContent): - request_info = await self.to_post_request_information(batch_request_content) elif isinstance(batch_request_content, BatchRequestContentCollection): - request_info = await self.to_post_request_information_from_collection( - batch_request_content - ) - batch_responses = await self.post_batch_collection(batch_request_content) + batch_responses = await self.post_batch_collection(batch_request_content, error_map) return batch_responses + else: raise ValueError("Invalid type for batch_request_content.") - # Decode and re-encode the content to ensure it is in the correct format - content = json.loads(request_info.content.decode("utf-8")) - json_body = json.dumps(content) - request_info.content = json_body.encode("utf-8") - - error_map = error_map or self.error_map - response = None - - try: - response = await self._request_adapter.send_async( - request_info, BatchResponseContent, error_map - ) - except APIError as e: - logging.error(f"API Error: {e}") - raise e - - if response is None: - raise ValueError("Failed to get a valid response from the API.") - - return response - async def post_batch_collection( self, batch_request_content_collection: BatchRequestContentCollection, @@ -137,7 +82,8 @@ async def post_batch_collection( Sends a collection of batch requests and returns a collection of batch response contents. Args: - batch_request_content_collection (BatchRequestContentCollection): The collection of batch request contents. + batch_request_content_collection (BatchRequestContentCollection): The + collection of batch request contents. error_map: Dict[str, int] = {}: Error mappings for response handling. @@ -189,37 +135,3 @@ async def to_post_request_information( ) return request_info - - async def to_post_request_information_from_collection( - self, batch_request_content: BatchRequestContentCollection - ) -> RequestInformation: - """ - Creates request information for a batch POST request from a collection. - - Args: - batch_request_content (BatchRequestContentCollection): The batch request content collection. - - Returns: - RequestInformation: The request information. - """ - if batch_request_content is None: - raise ValueError("batch_request_content cannot be Null.") - - request_info = RequestInformation() - request_info.http_method = Method.POST - request_info.url_template = self.url_template - - all_requests = [] - for batch_content in batch_request_content.batches: - requests_dict = [item.get_field_deserializers() for item in batch_content.requests] - all_requests.extend(requests_dict) - - request_info.content = json.dumps({"requests": all_requests}).encode("utf-8") - - request_info.headers = HeadersCollection() - request_info.headers.try_add("Content-Type", APPLICATION_JSON) - request_info.set_content_from_parsable( - self._request_adapter, APPLICATION_JSON, batch_request_content - ) - - return request_info From a21b8404c5d8da300e7b331f2822b401f643f236 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 00:03:40 +0300 Subject: [PATCH 48/68] clean up add post collection --- src/msgraph_core/requests/batch_request_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 61520522..5889726e 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -66,7 +66,7 @@ async def post( if response is None: raise ValueError("Failed to get a valid response from the API.") return response - elif isinstance(batch_request_content, BatchRequestContentCollection): + if isinstance(batch_request_content, BatchRequestContentCollection): batch_responses = await self.post_batch_collection(batch_request_content, error_map) return batch_responses From 333d168b21d4e3bde7741a0d1a10b0d833db6177 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 00:07:54 +0300 Subject: [PATCH 49/68] clean up post collection --- src/msgraph_core/requests/batch_request_builder.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 5889726e..313428de 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Dict, Optional, Union, List +from typing import TypeVar, Dict, Optional, Union import json import logging @@ -70,8 +70,7 @@ async def post( batch_responses = await self.post_batch_collection(batch_request_content, error_map) return batch_responses - else: - raise ValueError("Invalid type for batch_request_content.") + raise ValueError("Invalid type for batch_request_content.") async def post_batch_collection( self, From 69dd264d316f20dddab29436fb3221c9a68b009d Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 00:14:23 +0300 Subject: [PATCH 50/68] remove duplicate method --- .../requests/batch_response_content.py | 16 ++++++++-------- tests/requests/test_batch_response_content.py | 7 ------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index ecb5e9e5..3a7e3ff1 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -58,13 +58,13 @@ def response(self, request_id: str) -> 'BatchResponseItem': return response raise KeyError(f"Response with request ID {request_id} not found.") - def get_response_by_id(self, request_id: str) -> 'BatchResponseItem': - if self._responses is None: - raise ValueError("Responses list is not initialized.") - for response in self._responses: - if response.request_id == request_id: - return response - raise KeyError(f"Response with request ID {request_id} not found.") + # def get_response_by_id(self, request_id: str) -> 'BatchResponseItem': + # if self._responses is None: + # raise ValueError("Responses list is not initialized.") + # for response in self._responses: + # if response.request_id == request_id: + # return response + # raise KeyError(f"Response with request ID {request_id} not found.") def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: """ @@ -82,7 +82,7 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: if not issubclass(type, Parsable): raise ValueError("Type passed must implement the Parsable interface") - response = self.get_response_by_id(request_id) + response = self.response(request_id) content_type = response.content_type if not content_type: raise RuntimeError("Unable to get content-type header in response item") diff --git a/tests/requests/test_batch_response_content.py b/tests/requests/test_batch_response_content.py index 4c416dca..397a086a 100644 --- a/tests/requests/test_batch_response_content.py +++ b/tests/requests/test_batch_response_content.py @@ -28,13 +28,6 @@ def test_response_method(batch_response_content): assert batch_response_content.response("12345") == response_item -def test_get_response_by_id_method(batch_response_content): - response_item = Mock(spec=BatchResponseItem) - response_item.request_id = "12345" - batch_response_content.responses = [response_item] - assert batch_response_content.get_response_by_id("12345") == response_item - - def test_response_body_method(batch_response_content): response_item = Mock(spec=BatchResponseItem) response_item.request_id = "12345" From 18a77d209baa08165d1ea28461e58985bb3867c3 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 12:37:54 +0300 Subject: [PATCH 51/68] allow passing a Parsable as response_type --- .../requests/batch_request_builder.py | 16 ++++++++++++---- .../requests/batch_request_content.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 313428de..7a489b61 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -34,21 +34,29 @@ def __init__(self, request_adapter: RequestAdapter, error_map: Optional[Dict[str async def post( self, batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], + response_type: Optional[T] = None, error_map: Optional[Dict[str, int]] = None, - ) -> BatchResponseContent: + ) -> Union[T, BatchResponseContentCollection]: """ Sends a batch request and returns the batch response content. Args: - batch_request_content (BatchRequestContent): The batch request content. + batch_request_content (Union[BatchRequestContent, + BatchRequestContentCollection]): The batch request content. + response_type (Optional[T]): The type to deserialize the response into. error_map: Dict[str, int] = {}: Error mappings for response handling. Returns: - BatchResponseContent: The batch response content. + Union[T, BatchResponseContentCollection]: The batch response content + or the specified response type. + """ if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") + if response_type is None: + response_type = BatchResponseContent + if isinstance(batch_request_content, BatchRequestContent): request_info = await self.to_post_request_information(batch_request_content) content = json.loads(request_info.content.decode("utf-8")) @@ -58,7 +66,7 @@ async def post( response = None try: response = await self._request_adapter.send_async( - request_info, BatchResponseContent, error_map + request_info, response_type, error_map ) except APIError as e: logging.error("API Error: %s", e) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 840a3f65..2518a871 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -13,7 +13,7 @@ class BatchRequestContent(Parsable): Provides operations to call the batch method. """ - MAX_REQUESTS = 5 + MAX_REQUESTS = 20 def __init__(self, requests: List[Union['BatchRequestItem', 'RequestInformation']] = []): """ From 60bebdd669a0bb2f51cc59b5825ece7ead20a773 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 13:13:29 +0300 Subject: [PATCH 52/68] add response type to post method --- src/msgraph_core/requests/batch_request_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 7a489b61..50a19934 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Dict, Optional, Union +from typing import TypeVar, Type, Dict, Optional, Union import json import logging @@ -34,7 +34,7 @@ def __init__(self, request_adapter: RequestAdapter, error_map: Optional[Dict[str async def post( self, batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], - response_type: Optional[T] = None, + response_type: Optional[Type[T]] = None, error_map: Optional[Dict[str, int]] = None, ) -> Union[T, BatchResponseContentCollection]: """ @@ -43,7 +43,7 @@ async def post( Args: batch_request_content (Union[BatchRequestContent, BatchRequestContentCollection]): The batch request content. - response_type (Optional[T]): The type to deserialize the response into. + response_type: Optional[Type[T]] : The type to deserialize the response into. error_map: Dict[str, int] = {}: Error mappings for response handling. From 52998f20d0e6f5f574dc9b876e37abad31f7671d Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 14:22:02 +0300 Subject: [PATCH 53/68] add custom error_map optional parameter --- .../requests/batch_request_builder.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 50a19934..578b6e4d 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -24,7 +24,11 @@ class BatchRequestBuilder: Provides operations to call the batch method. """ - def __init__(self, request_adapter: RequestAdapter, error_map: Optional[Dict[str, int]] = None): + def __init__( + self, + request_adapter: RequestAdapter, + error_map: Optional[Dict[str, Type[Parsable]]] = None + ): if request_adapter is None: raise ValueError("request_adapter cannot be Null.") self._request_adapter = request_adapter @@ -35,7 +39,7 @@ async def post( self, batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], response_type: Optional[Type[T]] = None, - error_map: Optional[Dict[str, int]] = None, + error_map: Optional[Dict[str, Type[Parsable]]] = None, ) -> Union[T, BatchResponseContentCollection]: """ Sends a batch request and returns the batch response content. @@ -44,7 +48,7 @@ async def post( batch_request_content (Union[BatchRequestContent, BatchRequestContentCollection]): The batch request content. response_type: Optional[Type[T]] : The type to deserialize the response into. - error_map: Dict[str, int] = {}: + Optional[Dict[str, Type[Parsable]]] = None: Error mappings for response handling. Returns: @@ -83,7 +87,7 @@ async def post( async def post_batch_collection( self, batch_request_content_collection: BatchRequestContentCollection, - error_map: Optional[Dict[str, int]] = None, + error_map: Optional[Dict[str, Type[Parsable]]] = None, ) -> BatchResponseContentCollection: """ Sends a collection of batch requests and returns a collection of batch response contents. @@ -91,7 +95,7 @@ async def post_batch_collection( Args: batch_request_content_collection (BatchRequestContentCollection): The collection of batch request contents. - error_map: Dict[str, int] = {}: + Optional[Dict[str, Type[Parsable]]] = None: Error mappings for response handling. Returns: @@ -105,7 +109,7 @@ async def post_batch_collection( for batch_request_content in batch_request_content_collection.batches: request_info = await self.to_post_request_information(batch_request_content) response = await self._request_adapter.send_async( - request_info, BatchResponseContent, error_map + request_info, BatchResponseContent, error_map or self.error_map ) batch_responses.add_response(response) From ca19ad8fec2d21f4d723c22c8f85550c61386219 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 15:17:02 +0300 Subject: [PATCH 54/68] consolidate json serilization of request body --- src/msgraph_core/requests/batch_request_builder.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 578b6e4d..ca6bc749 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -63,9 +63,9 @@ async def post( if isinstance(batch_request_content, BatchRequestContent): request_info = await self.to_post_request_information(batch_request_content) - content = json.loads(request_info.content.decode("utf-8")) - json_body = json.dumps(content) - request_info.content = json_body + # content = json.loads(request_info.content.decode("utf-8")) + # json_body = json.dumps(content) + # request_info.content = json_body error_map = error_map or self.error_map response = None try: @@ -138,7 +138,6 @@ async def to_post_request_information( item.get_field_deserializers() for item in batch_request_content.requests ] request_info.content = json.dumps({"requests": requests_dict}).encode("utf-8") - request_info.headers = HeadersCollection() request_info.headers.try_add("Content-Type", APPLICATION_JSON) request_info.set_content_from_parsable( From bf6133c2fe1f252166d122af27e28da5da853a1e Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 15:22:31 +0300 Subject: [PATCH 55/68] remove redundant body serialization code --- src/msgraph_core/requests/batch_request_builder.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index ca6bc749..e9746e42 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -133,11 +133,6 @@ async def to_post_request_information( request_info = RequestInformation() request_info.http_method = Method.POST request_info.url_template = self.url_template - - requests_dict = [ - item.get_field_deserializers() for item in batch_request_content.requests - ] - request_info.content = json.dumps({"requests": requests_dict}).encode("utf-8") request_info.headers = HeadersCollection() request_info.headers.try_add("Content-Type", APPLICATION_JSON) request_info.set_content_from_parsable( From d9678f41f83a7ea5714a1a0fc7f0835831703f2c Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 11 Sep 2024 22:38:41 +0300 Subject: [PATCH 56/68] code clean up --- src/msgraph_core/requests/batch_request_builder.py | 4 ++-- src/msgraph_core/requests/batch_request_content_collection.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index e9746e42..c9a9b4d9 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -79,12 +79,12 @@ async def post( raise ValueError("Failed to get a valid response from the API.") return response if isinstance(batch_request_content, BatchRequestContentCollection): - batch_responses = await self.post_batch_collection(batch_request_content, error_map) + batch_responses = await self._post_batch_collection(batch_request_content, error_map) return batch_responses raise ValueError("Invalid type for batch_request_content.") - async def post_batch_collection( + async def _post_batch_collection( self, batch_request_content_collection: BatchRequestContentCollection, error_map: Optional[Dict[str, Type[Parsable]]] = None, diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index 532131b2..da663dea 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -13,8 +13,7 @@ class BatchRequestContentCollection: def __init__(self) -> None: """ Initializes a new instance of the BatchRequestContentCollection class. - Args: - number of requests in a batch. Defaults to 20. + """ self.batches: List[BatchRequestContent] = [] From e40450ed142d27bac7b5a681fa8932adb4097209 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Thu, 12 Sep 2024 11:49:05 +0300 Subject: [PATCH 57/68] add depends on validation in add request --- src/msgraph_core/requests/batch_request_content.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 2518a871..cf6ff223 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -47,6 +47,12 @@ def add_request(self, request: BatchRequestItem) -> None: raise RuntimeError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") if not request.id: request.id = str(uuid.uuid4()) + if hasattr(request, 'depends_on') and request.depends_on: + for dependent_id in request.depends_on: + if dependent_id not in [req.id for req in self.requests]: + dependent_request = self.requests.request(dependent_id) + if dependent_request: + self._requests.append(dependent_request) self._requests.append(request) def add_request_information(self, request_information: RequestInformation) -> None: From 5fdc6c52b547c064c452f96560478f1b2f48e7b6 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Thu, 12 Sep 2024 12:11:39 +0300 Subject: [PATCH 58/68] validate depends on at adding and remocing requests --- .../requests/batch_request_content.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index cf6ff223..31da6bc6 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -50,7 +50,7 @@ def add_request(self, request: BatchRequestItem) -> None: if hasattr(request, 'depends_on') and request.depends_on: for dependent_id in request.depends_on: if dependent_id not in [req.id for req in self.requests]: - dependent_request = self.requests.request(dependent_id) + dependent_request = self._request_by_id(dependent_id) if dependent_request: self._requests.append(dependent_request) self._requests.append(request) @@ -72,12 +72,20 @@ def add_urllib_request(self, request) -> None: def remove(self, request_id: str) -> None: """ Removes a request from the batch request content. + Also removes the request from the depends_on list of + other requests. """ + request_to_remove = None for request in self.requests: if request.id == request_id: - self.requests.remove(request) - return - raise ValueError(f"Request ID {request_id} not found in requests.") + request_to_remove = request + if hasattr(request, 'depends_on') and request.depends_on: + if request_id in request.depends_on: + request.depends_on.remove(request_id) + if request_to_remove: + self._requests.remove(request_to_remove) + else: + raise ValueError(f"Request ID {request_id} not found in requests.") def remove_batch_request_item(self, item: BatchRequestItem) -> None: """ @@ -92,6 +100,21 @@ def finalize(self): self.is_finalized = True return self._requests + def _request_by_id(self, request_id: str) -> Optional[BatchRequestItem]: + """ + Finds a request by its ID. + + Args: + request_id (str): The ID of the request to find. + + Returns: + The request with the given ID, or None if not found. + """ + for req in self.requests: + if req.id == request_id: + return req + return None + @staticmethod def create_from_discriminator_value( parse_node: Optional[ParseNode] = None From 5c5286ea7eac11b8b92c30c08cb29caf449297de Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Thu, 12 Sep 2024 13:00:29 +0300 Subject: [PATCH 59/68] convert requests to dictionary in batch request content --- .../requests/batch_request_content.py | 22 +++++++++---------- tests/requests/test_batch_request_content.py | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 31da6bc6..40a8126b 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -1,5 +1,5 @@ import uuid -from typing import List, Dict, Union, Any, Optional +from typing import List, Dict, Union, Optional from kiota_abstractions.request_information import RequestInformation from kiota_abstractions.serialization import Parsable, ParseNode @@ -19,15 +19,18 @@ def __init__(self, requests: List[Union['BatchRequestItem', 'RequestInformation' """ Initializes a new instance of the BatchRequestContent class. """ - self._requests: List[Union[BatchRequestItem, 'RequestInformation']] = requests or [] + self._requests: Dict[str, Union[BatchRequestItem, 'RequestInformation']] = {} + self.is_finalized = False + for request in requests: + self.add_request(request) @property def requests(self) -> List: """ Gets the requests. """ - return self._requests + return list(self._requests.values()) @requests.setter def requests(self, requests: List[BatchRequestItem]) -> None: @@ -52,8 +55,8 @@ def add_request(self, request: BatchRequestItem) -> None: if dependent_id not in [req.id for req in self.requests]: dependent_request = self._request_by_id(dependent_id) if dependent_request: - self._requests.append(dependent_request) - self._requests.append(request) + self._requests[dependent_id] = dependent_request + self._requests[request.id] = request def add_request_information(self, request_information: RequestInformation) -> None: """ @@ -83,7 +86,7 @@ def remove(self, request_id: str) -> None: if request_id in request.depends_on: request.depends_on.remove(request_id) if request_to_remove: - self._requests.remove(request_to_remove) + del self._requests[request_to_remove.id] else: raise ValueError(f"Request ID {request_id} not found in requests.") @@ -98,7 +101,7 @@ def finalize(self): Finalizes the batch request content. """ self.is_finalized = True - return self._requests + return list(self._requests.values()) def _request_by_id(self, request_id: str) -> Optional[BatchRequestItem]: """ @@ -110,10 +113,7 @@ def _request_by_id(self, request_id: str) -> Optional[BatchRequestItem]: Returns: The request with the given ID, or None if not found. """ - for req in self.requests: - if req.id == request_id: - return req - return None + return self._requests.get(request_id) @staticmethod def create_from_discriminator_value( diff --git a/tests/requests/test_batch_request_content.py b/tests/requests/test_batch_request_content.py index 504f8c98..df169b42 100644 --- a/tests/requests/test_batch_request_content.py +++ b/tests/requests/test_batch_request_content.py @@ -55,8 +55,8 @@ def test_initialization(batch_request_content, batch_request_item1, batch_reques def test_requests_property(batch_request_content, batch_request_item1, batch_request_item2): new_request_item = batch_request_item1 batch_request_content.requests = [batch_request_item1, batch_request_item2, new_request_item] - assert len(batch_request_content.requests) == 5 - assert batch_request_content.requests[2] == new_request_item + assert len(batch_request_content.requests) == 2 + assert batch_request_content.requests[0] == new_request_item def test_add_request(batch_request_content, batch_request_item1): From 13687c3c4ae6003681e27d4c4e6cebacbe0a10cb Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Thu, 12 Sep 2024 17:00:03 +0300 Subject: [PATCH 60/68] convert responses to dict --- .../requests/batch_response_content.py | 30 +++++++------------ tests/requests/test_batch_response_content.py | 7 +++-- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 3a7e3ff1..c6542743 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -20,7 +20,7 @@ def __init__(self) -> None: BatchResponseContent is a collection of BatchResponseItem items, each with a unique request ID. """ - self._responses: Optional[List['BatchResponseItem']] = [] + self._responses: Optional[Dict[str, 'BatchResponseItem']] = {} @property def responses(self) -> Optional[List['BatchResponseItem']]: @@ -38,10 +38,7 @@ def responses(self, responses: Optional[List['BatchResponseItem']]) -> None: :param responses: The responses to set in the collection :type responses: Optional[Dict[str, BatchResponseItem]] """ - if isinstance(responses, dict): - self._responses = {response.id: response for response in responses.values()} - else: - self._responses = responses + self._responses = responses def response(self, request_id: str) -> 'BatchResponseItem': """ @@ -53,19 +50,10 @@ def response(self, request_id: str) -> 'BatchResponseItem': """ if self._responses is None: raise ValueError("Responses list is not initialized.") - for response in self._responses: - if response.request_id == request_id: - return response + if request_id in self._responses: + return self._responses[request_id] raise KeyError(f"Response with request ID {request_id} not found.") - # def get_response_by_id(self, request_id: str) -> 'BatchResponseItem': - # if self._responses is None: - # raise ValueError("Responses list is not initialized.") - # for response in self._responses: - # if response.request_id == request_id: - # return response - # raise KeyError(f"Response with request ID {request_id} not found.") - def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: """ Get the body of a response by its request ID from the collection @@ -114,8 +102,11 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: """ return { 'responses': - lambda n: - setattr(self, 'responses', n.get_collection_of_object_values(BatchResponseItem)) + lambda n: setattr( + self, 'responses', + {item.id: item + for item in n.get_collection_of_object_values(BatchResponseItem)} + ) } def serialize(self, writer: SerializationWriter) -> None: @@ -123,7 +114,8 @@ def serialize(self, writer: SerializationWriter) -> None: Writes the objects properties to the current writer. :param writer: The writer to write to """ - writer.write_collection_of_object_values('responses', self._responses) + if self._responses: + writer.write_collection_of_object_values('responses', list(self._responses.values())) @staticmethod def create_from_discriminator_value( diff --git a/tests/requests/test_batch_response_content.py b/tests/requests/test_batch_response_content.py index 397a086a..13195a6f 100644 --- a/tests/requests/test_batch_response_content.py +++ b/tests/requests/test_batch_response_content.py @@ -12,7 +12,8 @@ def batch_response_content(): def test_initialization(batch_response_content): - assert batch_response_content.responses == [] + assert batch_response_content.responses == {} + assert isinstance(batch_response_content._responses, dict) def test_responses_property(batch_response_content): @@ -24,7 +25,7 @@ def test_responses_property(batch_response_content): def test_response_method(batch_response_content): response_item = Mock(spec=BatchResponseItem) response_item.request_id = "12345" - batch_response_content.responses = [response_item] + batch_response_content.responses = {"12345": response_item} assert batch_response_content.response("12345") == response_item @@ -53,7 +54,7 @@ def test_get_field_deserializers(batch_response_content): def test_serialize(batch_response_content): writer = Mock(spec=SerializationWriter) response_item = Mock(spec=BatchResponseItem) - batch_response_content.responses = [response_item] + batch_response_content.responses = {"12345": response_item} batch_response_content.serialize(writer) writer.write_collection_of_object_values.assert_called_once_with('responses', [response_item]) From 37ceeaed4ef8cff04db144934047f256d13d99e4 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Thu, 12 Sep 2024 17:06:02 +0300 Subject: [PATCH 61/68] add type annotations --- src/msgraph_core/requests/batch_response_content.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index c6542743..e74433b4 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -23,7 +23,7 @@ def __init__(self) -> None: self._responses: Optional[Dict[str, 'BatchResponseItem']] = {} @property - def responses(self) -> Optional[List['BatchResponseItem']]: + def responses(self) -> Optional[Dict[str, 'BatchResponseItem']]: """ Get the responses in the collection :return: A dictionary of response IDs and their BatchResponseItem objects @@ -32,7 +32,7 @@ def responses(self) -> Optional[List['BatchResponseItem']]: return self._responses @responses.setter - def responses(self, responses: Optional[List['BatchResponseItem']]) -> None: + def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None: """ Set the responses in the collection :param responses: The responses to set in the collection From e1f610d091ab9850ae19c5276fc5179528d120cb Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Thu, 12 Sep 2024 18:16:52 +0300 Subject: [PATCH 62/68] optimize request collecton creation --- .../requests/batch_request_content_collection.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index da663dea..b6b6a69a 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -16,6 +16,7 @@ def __init__(self) -> None: """ + self.max_requests_per_batch = BatchRequestContent.MAX_REQUESTS self.batches: List[BatchRequestContent] = [] self.current_batch: BatchRequestContent = BatchRequestContent() @@ -25,14 +26,10 @@ def add_batch_request_item(self, request: BatchRequestItem) -> None: Args: request (BatchRequestItem): The request item to add. """ - try: - self.current_batch.add_request(request) - except ValueError as e: - if "Maximum number of requests is" in str(e): - self.batches.append(self.current_batch.finalize()) - - self.current_batch = BatchRequestContent() - self.current_batch.add_request(request) + if len(self.current_batch.requests) >= self.max_requests_per_batch: + self.batches.append(self.current_batch.finalize()) + self.current_batch = BatchRequestContent() + self.current_batch.add_request(request) self.batches.append(self.current_batch) def remove_batch_request_item(self, request_id: str) -> None: From e62565d2494c1667fbb50157c5e882dab46c6712 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 16 Sep 2024 17:52:42 +0300 Subject: [PATCH 63/68] clean up code after list to dict conversion --- .../requests/batch_request_builder.py | 14 ++++++++------ .../requests/batch_request_content.py | 11 +++++++---- .../requests/batch_response_content.py | 19 ++++++++++++------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index c9a9b4d9..5b12a0bb 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -38,7 +38,6 @@ def __init__( async def post( self, batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], - response_type: Optional[Type[T]] = None, error_map: Optional[Dict[str, Type[Parsable]]] = None, ) -> Union[T, BatchResponseContentCollection]: """ @@ -58,20 +57,21 @@ async def post( """ if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") - if response_type is None: - response_type = BatchResponseContent + response_type = BatchResponseContent if isinstance(batch_request_content, BatchRequestContent): request_info = await self.to_post_request_information(batch_request_content) - # content = json.loads(request_info.content.decode("utf-8")) - # json_body = json.dumps(content) - # request_info.content = json_body error_map = error_map or self.error_map response = None try: response = await self._request_adapter.send_async( request_info, response_type, error_map ) + print(f"Response type returned for content : {type(response)}") + print(f"Batch response responses returned for content : {response.responses}") + for key in response.responses: + print(f"Response key -id: {key}") + print(f"Response value: {response.response(key).body}") except APIError as e: logging.error("API Error: %s", e) raise e @@ -80,6 +80,7 @@ async def post( return response if isinstance(batch_request_content, BatchRequestContentCollection): batch_responses = await self._post_batch_collection(batch_request_content, error_map) + print(f"Response type returned for collection: {type(response)}") return batch_responses raise ValueError("Invalid type for batch_request_content.") @@ -111,6 +112,7 @@ async def _post_batch_collection( response = await self._request_adapter.send_async( request_info, BatchResponseContent, error_map or self.error_map ) + print(f"Response type for batch item: {type(response)}") batch_responses.add_response(response) return batch_responses diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index 40a8126b..fafa6cb9 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -15,15 +15,15 @@ class BatchRequestContent(Parsable): MAX_REQUESTS = 20 - def __init__(self, requests: List[Union['BatchRequestItem', 'RequestInformation']] = []): + def __init__(self, requests: Dict[str, Union['BatchRequestItem', 'RequestInformation']] = {}): """ Initializes a new instance of the BatchRequestContent class. """ self._requests: Dict[str, Union[BatchRequestItem, 'RequestInformation']] = {} self.is_finalized = False - for request in requests: - self.add_request(request) + for request_id, request in requests.items(): + self.add_request(request_id, request) @property def requests(self) -> List: @@ -42,10 +42,13 @@ def requests(self, requests: List[BatchRequestItem]) -> None: for request in requests: self.add_request(request) - def add_request(self, request: BatchRequestItem) -> None: + def add_request(self, request_id: Optional[str], request: BatchRequestItem) -> None: """ Adds a request to the batch request content. """ + print(f"Request: {request}") + print(f"Request type: {type(request)}") + if len(self.requests) >= BatchRequestContent.MAX_REQUESTS: raise RuntimeError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") if not request.id: diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index e74433b4..e2badd15 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -40,7 +40,11 @@ def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None """ self._responses = responses - def response(self, request_id: str) -> 'BatchResponseItem': + def response( + self, + request_id: str, + response_type: Optional[Type[T]] = None, + ) -> 'BatchResponseItem': """ Get a response by its request ID from the collection :param request_id: The request ID of the response to get @@ -48,11 +52,12 @@ def response(self, request_id: str) -> 'BatchResponseItem': :return: The response with the specified request ID as a BatchResponseItem :rtype: BatchResponseItem """ - if self._responses is None: - raise ValueError("Responses list is not initialized.") - if request_id in self._responses: - return self._responses[request_id] - raise KeyError(f"Response with request ID {request_id} not found.") + return self._responses.get(request_id) + # if self._responses is None: + # raise ValueError("Responses list is not initialized.") + # if request_id in self._responses: + # return self._responses[request_id] + # raise KeyError(f"Response with request ID {request_id} not found.") def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: """ @@ -115,7 +120,7 @@ def serialize(self, writer: SerializationWriter) -> None: :param writer: The writer to write to """ if self._responses: - writer.write_collection_of_object_values('responses', list(self._responses.values())) + writer.write_collection_of_object_values('responses', self.responses()) @staticmethod def create_from_discriminator_value( From 3c8dc8f780152a644e0e72f2211e229ab1aae994 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 16 Sep 2024 19:56:11 +0300 Subject: [PATCH 64/68] fix batch request builder for updated requests in dictionaries --- .../requests/batch_request_builder.py | 32 +++++++++------- .../requests/batch_request_content.py | 8 ++-- .../requests/batch_response_content.py | 37 ++++++++++++++----- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 5b12a0bb..508e5d5d 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -61,6 +61,8 @@ async def post( if isinstance(batch_request_content, BatchRequestContent): request_info = await self.to_post_request_information(batch_request_content) + request_info.content = request_info.content.strip(b'[]') + print(f"Request info: {request_info.content}") error_map = error_map or self.error_map response = None try: @@ -69,9 +71,9 @@ async def post( ) print(f"Response type returned for content : {type(response)}") print(f"Batch response responses returned for content : {response.responses}") - for key in response.responses: - print(f"Response key -id: {key}") - print(f"Response value: {response.response(key).body}") + # for key in response.responses: + # print(f"Response key -id: {key}") + # print(f"Response value: {response.response(key).body}") except APIError as e: logging.error("API Error: %s", e) raise e @@ -129,16 +131,18 @@ async def to_post_request_information( Returns: RequestInformation: The request information. """ + if batch_request_content is None: raise ValueError("batch_request_content cannot be Null.") - if isinstance(batch_request_content, BatchRequestContent): - request_info = RequestInformation() - request_info.http_method = Method.POST - request_info.url_template = self.url_template - request_info.headers = HeadersCollection() - request_info.headers.try_add("Content-Type", APPLICATION_JSON) - request_info.set_content_from_parsable( - self._request_adapter, APPLICATION_JSON, batch_request_content - ) - - return request_info + batch_request_items = list(batch_request_content.requests.values()) + + request_info = RequestInformation() + request_info.http_method = Method.POST + request_info.url_template = self.url_template + request_info.headers = HeadersCollection() + request_info.headers.try_add("Content-Type", APPLICATION_JSON) + request_info.set_content_from_parsable( + self._request_adapter, APPLICATION_JSON, batch_request_items + ) + + return request_info diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index fafa6cb9..d0cefd6d 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -19,18 +19,18 @@ def __init__(self, requests: Dict[str, Union['BatchRequestItem', 'RequestInforma """ Initializes a new instance of the BatchRequestContent class. """ - self._requests: Dict[str, Union[BatchRequestItem, 'RequestInformation']] = {} + self._requests: Dict[str, Union[BatchRequestItem, 'RequestInformation']] = requests or {} self.is_finalized = False for request_id, request in requests.items(): self.add_request(request_id, request) @property - def requests(self) -> List: + def requests(self) -> Dict: """ Gets the requests. """ - return list(self._requests.values()) + return self._requests @requests.setter def requests(self, requests: List[BatchRequestItem]) -> None: @@ -104,7 +104,7 @@ def finalize(self): Finalizes the batch request content. """ self.is_finalized = True - return list(self._requests.values()) + return self._requests def _request_by_id(self, request_id: str) -> Optional[BatchRequestItem]: """ diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index e2badd15..c1f22fc9 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -40,6 +40,20 @@ def responses(self, responses: Optional[Dict[str, 'BatchResponseItem']]) -> None """ self._responses = responses + def get_response_by_id( + self, + request_id: str, + response_type: Optional[Type[T]] = None, + ) -> 'BatchResponseItem': + """ + Get a response by its request ID from the collection + :param request_id: The request ID of the response to get + :type request_id: str + :return: The response with the specified request ID as a BatchResponseItem + :rtype: BatchResponseItem + """ + return self._responses.get(request_id) + def response( self, request_id: str, @@ -105,22 +119,27 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: :return: The deserialization information for this object :rtype: Dict[str, Callable[[ParseNode], None]] """ - return { - 'responses': - lambda n: setattr( - self, 'responses', - {item.id: item - for item in n.get_collection_of_object_values(BatchResponseItem)} - ) + fields = { + "responses": + lambda n: + setattr(self, 'responses', n.get_collection_of_object_values(BatchResponseItem)) } + return fields + # return { + # 'responses': + # lambda n: setattr( + # self, '_responses', + # {item.id: item + # for item in n.get_collection_of_object_values(BatchResponseItem)} + # ) + # } def serialize(self, writer: SerializationWriter) -> None: """ Writes the objects properties to the current writer. :param writer: The writer to write to """ - if self._responses: - writer.write_collection_of_object_values('responses', self.responses()) + writer.write_collection_of_object_values('responses', self._responses) @staticmethod def create_from_discriminator_value( From f85ba50461c407b0adb7fa19801d7b7dce41f4e1 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Mon, 16 Sep 2024 23:47:31 +0300 Subject: [PATCH 65/68] update unit tests --- .../requests/batch_request_builder.py | 16 +++---- .../requests/batch_request_content.py | 11 ++--- .../batch_request_content_collection.py | 15 +++--- .../requests/batch_response_content.py | 48 ++++++++++--------- tests/requests/test_batch_request_content.py | 26 ++++------ 5 files changed, 52 insertions(+), 64 deletions(-) diff --git a/src/msgraph_core/requests/batch_request_builder.py b/src/msgraph_core/requests/batch_request_builder.py index 508e5d5d..608876f0 100644 --- a/src/msgraph_core/requests/batch_request_builder.py +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -1,5 +1,4 @@ from typing import TypeVar, Type, Dict, Optional, Union -import json import logging from kiota_abstractions.request_adapter import RequestAdapter @@ -61,19 +60,18 @@ async def post( if isinstance(batch_request_content, BatchRequestContent): request_info = await self.to_post_request_information(batch_request_content) - request_info.content = request_info.content.strip(b'[]') - print(f"Request info: {request_info.content}") + bytes_content = request_info.content + json_content = bytes_content.decode("utf-8") + updated_str = '{"requests":' + json_content + '}' + updated_bytes = updated_str.encode("utf-8") + request_info.content = updated_bytes error_map = error_map or self.error_map response = None try: response = await self._request_adapter.send_async( request_info, response_type, error_map ) - print(f"Response type returned for content : {type(response)}") - print(f"Batch response responses returned for content : {response.responses}") - # for key in response.responses: - # print(f"Response key -id: {key}") - # print(f"Response value: {response.response(key).body}") + except APIError as e: logging.error("API Error: %s", e) raise e @@ -82,7 +80,6 @@ async def post( return response if isinstance(batch_request_content, BatchRequestContentCollection): batch_responses = await self._post_batch_collection(batch_request_content, error_map) - print(f"Response type returned for collection: {type(response)}") return batch_responses raise ValueError("Invalid type for batch_request_content.") @@ -114,7 +111,6 @@ async def _post_batch_collection( response = await self._request_adapter.send_async( request_info, BatchResponseContent, error_map or self.error_map ) - print(f"Response type for batch item: {type(response)}") batch_responses.add_response(response) return batch_responses diff --git a/src/msgraph_core/requests/batch_request_content.py b/src/msgraph_core/requests/batch_request_content.py index d0cefd6d..9a48ef0f 100644 --- a/src/msgraph_core/requests/batch_request_content.py +++ b/src/msgraph_core/requests/batch_request_content.py @@ -40,15 +40,12 @@ def requests(self, requests: List[BatchRequestItem]) -> None: if len(requests) >= BatchRequestContent.MAX_REQUESTS: raise ValueError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") for request in requests: - self.add_request(request) + self.add_request(request.id, request) def add_request(self, request_id: Optional[str], request: BatchRequestItem) -> None: """ Adds a request to the batch request content. """ - print(f"Request: {request}") - print(f"Request type: {type(request)}") - if len(self.requests) >= BatchRequestContent.MAX_REQUESTS: raise RuntimeError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}") if not request.id: @@ -67,13 +64,15 @@ def add_request_information(self, request_information: RequestInformation) -> No Args: request_information (RequestInformation): The request information to add. """ - self.add_request(BatchRequestItem(request_information)) + request_id = str(uuid.uuid4()) + self.add_request(request_id, BatchRequestItem(request_information)) def add_urllib_request(self, request) -> None: """ Adds a request to the batch request content. """ - self.add_request(BatchRequestItem.create_with_urllib_request(request)) + request_id = str(uuid.uuid4()) + self.add_request(request_id, BatchRequestItem.create_with_urllib_request(request)) def remove(self, request_id: str) -> None: """ diff --git a/src/msgraph_core/requests/batch_request_content_collection.py b/src/msgraph_core/requests/batch_request_content_collection.py index b6b6a69a..fa67540d 100644 --- a/src/msgraph_core/requests/batch_request_content_collection.py +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -29,7 +29,7 @@ def add_batch_request_item(self, request: BatchRequestItem) -> None: if len(self.current_batch.requests) >= self.max_requests_per_batch: self.batches.append(self.current_batch.finalize()) self.current_batch = BatchRequestContent() - self.current_batch.add_request(request) + self.current_batch.add_request(request.id, request) self.batches.append(self.current_batch) def remove_batch_request_item(self, request_id: str) -> None: @@ -39,14 +39,11 @@ def remove_batch_request_item(self, request_id: str) -> None: request_id (str): The ID of the request item to remove. """ for batch in self.batches: - for request in batch.requests: - if request.id == request_id: - batch.requests.remove(request) - return - for request in self.current_batch.requests: - if request.id == request_id: - self.current_batch.requests.remove(request) + if request_id in batch.requests: + del batch.requests[request_id] return + if request_id in self.current_batch.requests: + del self.current_batch.requests[request_id] def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: """ @@ -60,7 +57,7 @@ def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]: for request in batch.requests: if request.status_code not in [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]: if batch_with_failed_responses is not None: - batch_with_failed_responses.add_request(request) + batch_with_failed_responses.add_request(request.id, request) else: raise ValueError("batch_with_failed_responses is None") return batch_with_failed_responses diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index c1f22fc9..8085f0e9 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -44,7 +44,7 @@ def get_response_by_id( self, request_id: str, response_type: Optional[Type[T]] = None, - ) -> 'BatchResponseItem': + ) -> Optional['BatchResponseItem']: """ Get a response by its request ID from the collection :param request_id: The request ID of the response to get @@ -52,13 +52,17 @@ def get_response_by_id( :return: The response with the specified request ID as a BatchResponseItem :rtype: BatchResponseItem """ + if self._responses is None: + return None + if response_type is not None: + return response_type.create_from_discriminator_value(self._responses.get(request_id)) return self._responses.get(request_id) def response( self, request_id: str, response_type: Optional[Type[T]] = None, - ) -> 'BatchResponseItem': + ) -> Optional['BatchResponseItem']: """ Get a response by its request ID from the collection :param request_id: The request ID of the response to get @@ -66,12 +70,9 @@ def response( :return: The response with the specified request ID as a BatchResponseItem :rtype: BatchResponseItem """ + if self._responses is None: + return None return self._responses.get(request_id) - # if self._responses is None: - # raise ValueError("Responses list is not initialized.") - # if request_id in self._responses: - # return self._responses[request_id] - # raise KeyError(f"Response with request ID {request_id} not found.") def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: """ @@ -90,7 +91,12 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: raise ValueError("Type passed must implement the Parsable interface") response = self.response(request_id) - content_type = response.content_type + if response is not None: + content_type = response.content_type + else: + raise ValueError( + f"Unable to get content-type header in response item for request Id: {request_id}" + ) if not content_type: raise RuntimeError("Unable to get content-type header in response item") @@ -119,27 +125,25 @@ def get_field_deserializers(self) -> Dict[str, Callable[[ParseNode], None]]: :return: The deserialization information for this object :rtype: Dict[str, Callable[[ParseNode], None]] """ - fields = { - "responses": - lambda n: - setattr(self, 'responses', n.get_collection_of_object_values(BatchResponseItem)) + + return { + 'responses': + lambda n: setattr( + self, '_responses', + {item.id: item + for item in n.get_collection_of_object_values(BatchResponseItem)} + ) } - return fields - # return { - # 'responses': - # lambda n: setattr( - # self, '_responses', - # {item.id: item - # for item in n.get_collection_of_object_values(BatchResponseItem)} - # ) - # } def serialize(self, writer: SerializationWriter) -> None: """ Writes the objects properties to the current writer. :param writer: The writer to write to """ - writer.write_collection_of_object_values('responses', self._responses) + if self._responses is not None: + writer.write_collection_of_object_values('responses', list(self._responses.values())) + else: + writer.write_collection_of_object_values('responses', []) # type: ignore @staticmethod def create_from_discriminator_value( diff --git a/tests/requests/test_batch_request_content.py b/tests/requests/test_batch_request_content.py index df169b42..3ee241af 100644 --- a/tests/requests/test_batch_request_content.py +++ b/tests/requests/test_batch_request_content.py @@ -43,28 +43,31 @@ def batch_request_item2(request_info2): @pytest.fixture def batch_request_content(batch_request_item1, batch_request_item2): - return BatchRequestContent(requests=[batch_request_item1, batch_request_item2]) + return BatchRequestContent( + { + batch_request_item1.id: batch_request_item1, + batch_request_item2.id: batch_request_item2 + } + ) def test_initialization(batch_request_content, batch_request_item1, batch_request_item2): assert len(batch_request_content.requests) == 2 - assert batch_request_content.requests[0] == batch_request_item1 - assert batch_request_content.requests[1] == batch_request_item2 def test_requests_property(batch_request_content, batch_request_item1, batch_request_item2): new_request_item = batch_request_item1 batch_request_content.requests = [batch_request_item1, batch_request_item2, new_request_item] assert len(batch_request_content.requests) == 2 - assert batch_request_content.requests[0] == new_request_item + assert batch_request_content.requests[batch_request_item1.id] == new_request_item def test_add_request(batch_request_content, batch_request_item1): new_request_item = request_info1 new_request_item.id = "new_id" - batch_request_content.add_request(new_request_item) + batch_request_content.add_request(new_request_item.id, new_request_item) assert len(batch_request_content.requests) == 3 - assert batch_request_content.requests[-1] == new_request_item + assert batch_request_content.requests[new_request_item.id] == new_request_item def test_add_request_information(batch_request_content): @@ -81,17 +84,6 @@ def test_add_urllib_request(batch_request_content): urllib_request.data = b'{"key": "value"}' batch_request_content.add_urllib_request(urllib_request) assert len(batch_request_content.requests) == 3 - assert batch_request_content.requests[-1].method == "PATCH" - - -def test_remove(batch_request_content, batch_request_item1): - batch_request_content.remove(batch_request_item1.id) - assert len(batch_request_content.requests) == 1 - - -def test_remove_batch_request_item(batch_request_content, batch_request_item1): - batch_request_content.remove_batch_request_item(batch_request_item1) - assert len(batch_request_content.requests) == 1 def test_finalize(batch_request_content): From 93fda89427f27a84d8ff6701cf6531502c24f495 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 18 Sep 2024 22:08:41 +0300 Subject: [PATCH 66/68] add get response body as stream content --- .../requests/batch_response_content.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 8085f0e9..4ffc8a2c 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -58,6 +58,20 @@ def get_response_by_id( return response_type.create_from_discriminator_value(self._responses.get(request_id)) return self._responses.get(request_id) + def get_response_stream_by_id(self, request_id: str) -> Optional[BytesIO]: + """ + Get a response by its request ID and return the body as a stream + :param request_id: The request ID of the response to get + :type request_id: str + :return: The response Body as a stream + :rtype: io.BytesIO + """ + response_item = self.get_response_by_id(request_id) + if response_item is None or response_item.body is None: + return None + + return BytesIO(response_item.body) + def response( self, request_id: str, From 17089979258a7c4902b26b9460b88f8357082d83 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 18 Sep 2024 22:17:03 +0300 Subject: [PATCH 67/68] get content stream by id --- .../requests/batch_response_content.py | 22 ++++--------------- tests/requests/test_batch_response_content.py | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 4ffc8a2c..6e781de3 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, List, Type, TypeVar, Callable +from typing import Optional, Dict, Type, TypeVar, Callable from io import BytesIO import base64 @@ -70,24 +70,10 @@ def get_response_stream_by_id(self, request_id: str) -> Optional[BytesIO]: if response_item is None or response_item.body is None: return None + if isinstance(response_item.body, BytesIO): + return response_item.body return BytesIO(response_item.body) - def response( - self, - request_id: str, - response_type: Optional[Type[T]] = None, - ) -> Optional['BatchResponseItem']: - """ - Get a response by its request ID from the collection - :param request_id: The request ID of the response to get - :type request_id: str - :return: The response with the specified request ID as a BatchResponseItem - :rtype: BatchResponseItem - """ - if self._responses is None: - return None - return self._responses.get(request_id) - def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: """ Get the body of a response by its request ID from the collection @@ -104,7 +90,7 @@ def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: if not issubclass(type, Parsable): raise ValueError("Type passed must implement the Parsable interface") - response = self.response(request_id) + response = self.get_response_by_id(request_id) if response is not None: content_type = response.content_type else: diff --git a/tests/requests/test_batch_response_content.py b/tests/requests/test_batch_response_content.py index 13195a6f..49e44fc6 100644 --- a/tests/requests/test_batch_response_content.py +++ b/tests/requests/test_batch_response_content.py @@ -26,7 +26,7 @@ def test_response_method(batch_response_content): response_item = Mock(spec=BatchResponseItem) response_item.request_id = "12345" batch_response_content.responses = {"12345": response_item} - assert batch_response_content.response("12345") == response_item + assert batch_response_content.get_response_by_id("12345") == response_item def test_response_body_method(batch_response_content): From 378318fb792531af1b67e5ff7dc77d3813903016 Mon Sep 17 00:00:00 2001 From: shemogumbe Date: Wed, 18 Sep 2024 22:42:26 +0300 Subject: [PATCH 68/68] add get response status codes --- .../requests/batch_response_content.py | 18 ++++++- tests/requests/test_batch_response_content.py | 49 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/msgraph_core/requests/batch_response_content.py b/src/msgraph_core/requests/batch_response_content.py index 6e781de3..c7e78b54 100644 --- a/src/msgraph_core/requests/batch_response_content.py +++ b/src/msgraph_core/requests/batch_response_content.py @@ -64,7 +64,7 @@ def get_response_stream_by_id(self, request_id: str) -> Optional[BytesIO]: :param request_id: The request ID of the response to get :type request_id: str :return: The response Body as a stream - :rtype: io.BytesIO + :rtype: BytesIO """ response_item = self.get_response_by_id(request_id) if response_item is None or response_item.body is None: @@ -74,6 +74,22 @@ def get_response_stream_by_id(self, request_id: str) -> Optional[BytesIO]: return response_item.body return BytesIO(response_item.body) + def get_response_status_codes(self) -> Dict[str, int]: + """ + Go through responses and for each, append {'request-id': status_code} to a dictionary. + :return: A dictionary with request_id as keys and status_code as values. + :rtype: dict + """ + status_codes: Dict[str, int] = {} + if self._responses is None: + return status_codes + + for request_id, response_item in self._responses.items(): + if response_item is not None and response_item.status is not None: + status_codes[request_id] = response_item.status + + return status_codes + def response_body(self, request_id: str, type: Type[T]) -> Optional[T]: """ Get the body of a response by its request ID from the collection diff --git a/tests/requests/test_batch_response_content.py b/tests/requests/test_batch_response_content.py index 49e44fc6..9b8773cf 100644 --- a/tests/requests/test_batch_response_content.py +++ b/tests/requests/test_batch_response_content.py @@ -29,6 +29,55 @@ def test_response_method(batch_response_content): assert batch_response_content.get_response_by_id("12345") == response_item +def test_get_response_stream_by_id_none(batch_response_content): + batch_response_content.get_response_by_id = Mock(return_value=None) + result = batch_response_content.get_response_stream_by_id('1') + assert result is None + + +def test_get_response_stream_by_id_body_none(batch_response_content): + batch_response_content.get_response_by_id = Mock(return_value=Mock(body=None)) + result = batch_response_content.get_response_stream_by_id('1') + assert result is None + + +def test_get_response_stream_by_id_bytesio(batch_response_content): + batch_response_content.get_response_by_id = Mock( + return_value=Mock(body=BytesIO(b'Hello, world!')) + ) + result = batch_response_content.get_response_stream_by_id('2') + assert isinstance(result, BytesIO) + assert result.read() == b'Hello, world!' + + +def test_get_response_stream_by_id_bytes(batch_response_content): + batch_response_content.get_response_by_id = Mock(return_value=Mock(body=b'Hello, world!')) + result = batch_response_content.get_response_stream_by_id('1') + assert isinstance(result, BytesIO) + assert result.read() == b'Hello, world!' + + +def test_get_response_status_codes_none(batch_response_content): + batch_response_content._responses = None + result = batch_response_content.get_response_status_codes() + assert result == {} + + +def test_get_response_status_codes(batch_response_content): + batch_response_content._responses = { + '1': Mock(status=200), + '2': Mock(status=404), + '3': Mock(status=500), + } + result = batch_response_content.get_response_status_codes() + expected = { + '1': 200, + '2': 404, + '3': 500, + } + assert result == expected + + def test_response_body_method(batch_response_content): response_item = Mock(spec=BatchResponseItem) response_item.request_id = "12345"