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..608876f0 --- /dev/null +++ b/src/msgraph_core/requests/batch_request_builder.py @@ -0,0 +1,144 @@ +from typing import TypeVar, Type, Dict, Optional, Union +import logging + +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 .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') + +APPLICATION_JSON = "application/json" + + +class BatchRequestBuilder: + """ + Provides operations to call the batch method. + """ + + 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 + self.url_template = f"{self._request_adapter.base_url}/$batch" + self.error_map = error_map or {} + + async def post( + self, + batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection], + error_map: Optional[Dict[str, Type[Parsable]]] = None, + ) -> Union[T, BatchResponseContentCollection]: + """ + Sends a batch request and returns the batch response content. + + Args: + batch_request_content (Union[BatchRequestContent, + BatchRequestContentCollection]): The batch request content. + response_type: Optional[Type[T]] : The type to deserialize the response into. + Optional[Dict[str, Type[Parsable]]] = None: + Error mappings for response handling. + + Returns: + 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.") + response_type = BatchResponseContent + + if isinstance(batch_request_content, BatchRequestContent): + request_info = await self.to_post_request_information(batch_request_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 + ) + + except APIError as 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): + 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( + self, + batch_request_content_collection: BatchRequestContentCollection, + error_map: Optional[Dict[str, Type[Parsable]]] = 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. + Optional[Dict[str, Type[Parsable]]] = None: + 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) + response = await self._request_adapter.send_async( + request_info, BatchResponseContent, error_map or self.error_map + ) + batch_responses.add_response(response) + + return batch_responses + + 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.") + 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 new file mode 100644 index 00000000..9a48ef0f --- /dev/null +++ b/src/msgraph_core/requests/batch_request_content.py @@ -0,0 +1,140 @@ +import uuid +from typing import List, Dict, Union, Optional + +from kiota_abstractions.request_information import RequestInformation +from kiota_abstractions.serialization import Parsable, ParseNode +from kiota_abstractions.serialization import SerializationWriter + +from .batch_request_item import BatchRequestItem + + +class BatchRequestContent(Parsable): + """ + Provides operations to call the batch method. + """ + + MAX_REQUESTS = 20 + + def __init__(self, requests: Dict[str, Union['BatchRequestItem', 'RequestInformation']] = {}): + """ + Initializes a new instance of the BatchRequestContent class. + """ + 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) -> Dict: + """ + Gets the requests. + """ + return self._requests + + @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: + 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. + """ + 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()) + 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._request_by_id(dependent_id) + if dependent_request: + self._requests[dependent_id] = dependent_request + self._requests[request.id] = 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. + """ + 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. + """ + request_id = str(uuid.uuid4()) + self.add_request(request_id, BatchRequestItem.create_with_urllib_request(request)) + + 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: + 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: + del self._requests[request_to_remove.id] + else: + raise ValueError(f"Request ID {request_id} not found in requests.") + + 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 + + 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. + """ + return self._requests.get(request_id) + + @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: + """ + The deserialization information for the current model + """ + return {} + + def serialize(self, writer: SerializationWriter) -> None: + """ + Serializes information the current object + Args: + writer: Serialization writer to use to serialize this model + """ + writer.write_collection_of_object_values("requests", self.requests) 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..fa67540d --- /dev/null +++ b/src/msgraph_core/requests/batch_request_content_collection.py @@ -0,0 +1,84 @@ +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 + + +class BatchRequestContentCollection: + """A collection of request content objects.""" + + def __init__(self) -> None: + """ + Initializes a new instance of the BatchRequestContentCollection class. + + + """ + self.max_requests_per_batch = 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. + """ + 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.id, request) + 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: + 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]: + """ + 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: + 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.id, request) + else: + raise ValueError("batch_with_failed_responses is None") + 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) + 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_request_item.py b/src/msgraph_core/requests/batch_request_item.py new file mode 100644 index 00000000..e5f46e8b --- /dev/null +++ b/src/msgraph_core/requests/batch_request_item.py @@ -0,0 +1,247 @@ +import re +import json +from uuid import uuid4 +from typing import List, Optional, Dict, 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 +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import SerializationWriter +from kiota_abstractions.serialization import ParseNode + + +class StreamInterface(BytesIO): + pass + + +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: Optional[RequestInformation] = None, + id: str = "", + depends_on: Optional[List[Union[str, 'BatchRequestItem']]] = [] + ): + """ + 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 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 + self._headers = request_information.request_headers + self._body = request_information.content + self.url = request_information.url + self._depends_on: Optional[List[str]] = [] + if depends_on is not None: + 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': + """ + 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 + 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) + + 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: + 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: + """ + 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}") + + 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: + """ + 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: BytesIO) -> None: + """ + Sets the body of the request item. + Args: + body : (BytesIO): 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 + + @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]: + """ + 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 { + "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: + """ + 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) + writer.write_collection_of_primitive_values('depends_on', self._depends_on) + headers = {key: ", ".join(val) for key, val in self._headers.items()} + writer.write_collection_of_object_values('headers', headers) + if self._body: + json_object = json.loads(self._body) + is_json_string = json_object and isinstance(json_object, dict) + writer.write_collection_of_object_values( + 'body', + json_object if is_json_string else base64.b64encode(self._body).decode('utf-8') + ) 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..c7e78b54 --- /dev/null +++ b/src/msgraph_core/requests/batch_response_content.py @@ -0,0 +1,170 @@ +from typing import Optional, Dict, 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 ParseNodeFactoryRegistry +from kiota_abstractions.serialization import SerializationWriter + +from .batch_response_item import BatchResponseItem + +T = TypeVar('T', bound='Parsable') + + +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[Dict[str, '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 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]] + """ + self._responses = responses + + def get_response_by_id( + 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 + 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 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: BytesIO + """ + response_item = self.get_response_by_id(request_id) + 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 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 + :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}") + + if not issubclass(type, Parsable): + raise ValueError("Type passed must implement the Parsable interface") + + response = self.get_response_by_id(request_id) + 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") + + 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) + except Exception: + raise ValueError( + f"Unable to deserialize batch response for request Id: {request_id} to {type}" + ) + + 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( + 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 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( + 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_content_collection.py b/src/msgraph_core/requests/batch_response_content_collection.py new file mode 100644 index 00000000..e30ff30d --- /dev/null +++ b/src/msgraph_core/requests/batch_response_content_collection.py @@ -0,0 +1,82 @@ +from typing import Optional, Dict, Callable, List + +from kiota_abstractions.serialization import Parsable +from kiota_abstractions.serialization import ParseNode +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: + """ + 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: List[BatchResponseContent] = [] + + 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(response) + + def get_responses(self): + """ + Gets the responses in the collection. + Returns: + List[Tuple[str, BatchResponseContent]]: The responses in the collection. + """ + return self._responses + + @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): + 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]]: + """ + 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( + self, "_responses", + n. + get_collection_of_object_values(BatchResponseItem.create_from_discriminator_value) + ) + } + + 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) 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..91c39881 --- /dev/null +++ b/src/msgraph_core/requests/batch_response_item.py @@ -0,0 +1,161 @@ +from typing import Optional, Dict, Any +from io import BytesIO + +from kiota_abstractions.serialization import Parsable, ParsableFactory +from kiota_abstractions.serialization import ParseNode +from kiota_abstractions.serialization import SerializationWriter + + +class StreamInterface(BytesIO): + pass + + +class BatchResponseItem(Parsable): + + def __init__(self) -> None: + """ + Initializes a new instance of the BatchResponseItem class. + """ + self._id: Optional[str] = None + self._atomicity_group: Optional[str] = None + self._status: Optional[int] = None + self._headers: Optional[Dict[str, str]] = {} + self._body: Optional[BytesIO] = None + + @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(self) -> Optional[int]: + """ + Get the status code of the response + :return: The status code of the response + :rtype: Optional[int] + """ + return self._status + + @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 + :type status_code: Optional[int] + """ + self._status = 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[BytesIO]: + """ + Get the body of the response + :return: The body of the response + :rtype: Optional[BytesIO] + """ + 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 + + @staticmethod + def create_from_discriminator_value( + parse_node: Optional[ParseNode] = None + ) -> 'BatchResponseItem': + """ + 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]: + """ + Gets the deserialization information for this object. + + """ + 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.try_get_anything(x._json_node)), + "body": lambda x: setattr(self, "body", x.get_bytes_value()), + } + + 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', self._status) + writer.write_collection_of_primitive_values('headers', self._headers) + writer.write_bytes_value('body', self._body) 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_content.py b/tests/requests/test_batch_request_content.py new file mode 100644 index 00000000..3ee241af --- /dev/null +++ b/tests/requests/test_batch_request_content.py @@ -0,0 +1,111 @@ +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( + { + 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 + + +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[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.id, new_request_item) + assert len(batch_request_content.requests) == 3 + assert batch_request_content.requests[new_request_item.id] == 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 + + +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 + ) 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"] diff --git a/tests/requests/test_batch_response_content.py b/tests/requests/test_batch_response_content.py new file mode 100644 index 00000000..9b8773cf --- /dev/null +++ b/tests/requests/test_batch_response_content.py @@ -0,0 +1,114 @@ +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 == {} + assert isinstance(batch_response_content._responses, dict) + + +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 = {"12345": response_item} + 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" + 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 = {"12345": 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"} + )